diff --git a/strr-api/.vscode/settings.json b/strr-api/.vscode/settings.json index 9b388533a..4997a3be0 100644 --- a/strr-api/.vscode/settings.json +++ b/strr-api/.vscode/settings.json @@ -3,5 +3,7 @@ "tests" ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "python-envs.defaultEnvManager": "ms-python.python:poetry", + "python-envs.defaultPackageManager": "ms-python.python:poetry" } \ No newline at end of file diff --git a/strr-api/migrations/env.py b/strr-api/migrations/env.py index e69c29325..59c92dcef 100644 --- a/strr-api/migrations/env.py +++ b/strr-api/migrations/env.py @@ -1,104 +1,74 @@ +import os import logging from logging.config import fileConfig - -from flask import current_app - +from sqlalchemy import engine_from_config, pool, create_engine from alembic import context -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config = context.config +# 1. Passive Detection: Don't create an app, just look for one +try: + from flask import current_app + from strr_api import db + # If we are in a Flask context (like your Flask tests or running the web app) + if current_app: + use_flask = True + target_metadata = db.metadata + else: + raise ImportError +except (ImportError, RuntimeError): + # 2. Standalone Fallback: For your Jobs and Job-tests + use_flask = False + from strr_api.models.base_model import SimpleBaseModel + target_metadata = SimpleBaseModel.metadata -# Interpret the config file for Python logging. -# This line sets up loggers basically. -fileConfig(config.config_file_name) +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) logger = logging.getLogger('alembic.env') - def get_engine(): - return current_app.extensions['migrate'].db.engine - + if use_flask: + return current_app.extensions['migrate'].db.engine + # Standalone: use the env var you set in conftest or your job environment + url = os.getenv("DATABASE_URL") + return create_engine(url, poolclass=pool.NullPool) def get_engine_url(): - try: - return get_engine().url.render_as_string(hide_password=False).replace( - '%', '%%') - except AttributeError: + if use_flask: return str(get_engine().url).replace('%', '%%') - - -# add your model's MetaData object here -# for 'autogenerate' support -# from myapp import mymodel -# target_metadata = mymodel.Base.metadata -config.set_main_option('sqlalchemy.url', get_engine_url()) -target_db = current_app.extensions['migrate'].db - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# my_important_option = config.get_main_option("my_important_option") -# ... etc. - + return os.getenv("DATABASE_URL").replace('%', '%%') def get_metadata(): - if hasattr(target_db, 'metadatas'): - return target_db.metadatas[None] - return target_db.metadata - - -def run_migrations_offline(): - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - - """ - url = config.get_main_option("sqlalchemy.url") - context.configure( - url=url, target_metadata=get_metadata(), literal_binds=True - ) - - with context.begin_transaction(): - context.run_migrations() + return target_metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) def run_migrations_online(): - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine - and associate a connection with the context. - - """ - - # this callback is used to prevent an auto-migration from being generated - # when there are no changes to the schema - # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html - def process_revision_directives(context, revision, directives): - if getattr(config.cmd_opts, 'autogenerate', False): - script = directives[0] - if script.upgrade_ops.is_empty(): - directives[:] = [] - logger.info('No changes in schema detected.') - connectable = get_engine() - with connectable.connect() as connection: + # Only pull migrate args if Flask is actually present + extra_args = current_app.extensions['migrate'].configure_args if use_flask else {} + context.configure( connection=connection, target_metadata=get_metadata(), - process_revision_directives=process_revision_directives, - **current_app.extensions['migrate'].configure_args + **extra_args ) - with context.begin_transaction(): context.run_migrations() +def run_migrations_offline(): + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=get_metadata(), + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + if context.is_offline_mode(): run_migrations_offline() else: diff --git a/strr-api/poetry.lock b/strr-api/poetry.lock index 33d5da551..55f6a9aaf 100644 --- a/strr-api/poetry.lock +++ b/strr-api/poetry.lock @@ -843,6 +843,27 @@ files = [ {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, ] +[[package]] +name = "dnspython" +version = "2.8.0" +description = "DNS toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af"}, + {file = "dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f"}, +] + +[package.extras] +dev = ["black (>=25.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.17.0)", "mypy (>=1.17)", "pylint (>=3)", "pytest (>=8.4)", "pytest-cov (>=6.2.0)", "quart-trio (>=0.12.0)", "sphinx (>=8.2.0)", "sphinx-rtd-theme (>=3.0.0)", "twine (>=6.1.0)", "wheel (>=0.45.0)"] +dnssec = ["cryptography (>=45)"] +doh = ["h2 (>=4.2.0)", "httpcore (>=1.0.0)", "httpx (>=0.28.0)"] +doq = ["aioquic (>=1.2.0)"] +idna = ["idna (>=3.10)"] +trio = ["trio (>=0.30)"] +wmi = ["wmi (>=1.5.1) ; platform_system == \"Windows\""] + [[package]] name = "docker" version = "7.1.0" @@ -866,6 +887,22 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] ssh = ["paramiko (>=2.4.3)"] websockets = ["websocket-client (>=1.3.0)"] +[[package]] +name = "email-validator" +version = "2.3.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4"}, + {file = "email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + [[package]] name = "expiringdict" version = "1.2.2" @@ -2585,6 +2622,7 @@ files = [ [package.dependencies] annotated-types = ">=0.6.0" +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} pydantic-core = "2.41.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -4125,4 +4163,4 @@ test = ["pytest"] [metadata] lock-version = "2.1" python-versions = "^3.11" -content-hash = "0b56793b5fd619b64cf8bbe19dd2d62d2e5166bd846ed24ad2ea3432f3b47677" +content-hash = "894271489a2ce80d9e431a2e79262981fd4de58cf013724e4a134a74a594b211" diff --git a/strr-api/pyproject.toml b/strr-api/pyproject.toml index 880a5e1c5..962b69096 100644 --- a/strr-api/pyproject.toml +++ b/strr-api/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "strr-api" -version = "0.3.4" +version = "0.3.7" description = "" authors = ["thorwolpert "] license = "BSD 3-Clause" @@ -26,7 +26,7 @@ pytest-env = "^1.1.3" coloredlogs = "^15.0.1" flask-httpauth = "^4.8.0" flasgger = "^0.9.7.1" -pydantic = "^2.7.1" +pydantic = {extras = ["email"], version = "^2.12.5"} google-auth = "^2.29.0" google-cloud-storage = "2.14.0" geoalchemy2 = "^0.15.1" diff --git a/strr-api/src/strr_api/models/application.py b/strr-api/src/strr_api/models/application.py index 46dbdd274..ea7238a3d 100644 --- a/strr-api/src/strr_api/models/application.py +++ b/strr-api/src/strr_api/models/application.py @@ -216,6 +216,15 @@ def find_by_account( Application.type == ApplicationType.RENEWAL.value, ) ) + if filter_criteria.applications_only: + # Exclude applications that have a completed registration, except renewals + # Include: no registration yet or renewal applications + query = query.filter( + db.or_( + Application.registration_id.is_(None), + Application.type == ApplicationType.RENEWAL.value, + ) + ) sort_column = getattr(Application, filter_criteria.sort_by, Application.id) if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc": query = query.order_by(sort_column.asc()) diff --git a/strr-api/src/strr_api/models/dataclass.py b/strr-api/src/strr_api/models/dataclass.py index fb08dd535..0e6fada08 100644 --- a/strr-api/src/strr_api/models/dataclass.py +++ b/strr-api/src/strr_api/models/dataclass.py @@ -52,6 +52,7 @@ class ApplicationSearch: requirements: list[str] | None = None include_draft_registration: bool = True include_draft_renewal: bool = True + applications_only: bool = False account_id: int | None = None @@ -70,3 +71,6 @@ class RegistrationSearch: assignee: str | None = None requirements: list[str] | None = None account_id: int | None = None + approval_methods: List[str] | None = None + noc_statuses: List[str] | None = None + is_set_aside: bool | None = None diff --git a/strr-api/src/strr_api/models/rental.py b/strr-api/src/strr_api/models/rental.py index cd5ed42a1..f4239bf23 100644 --- a/strr-api/src/strr_api/models/rental.py +++ b/strr-api/src/strr_api/models/rental.py @@ -141,6 +141,12 @@ def search_registrations(cls, filter_criteria: RegistrationSearch): ) if filter_criteria.requirements: query = cls._filter_by_registration_requirement(filter_criteria.requirements, query) + if filter_criteria.approval_methods: + query = cls._filter_by_approval_method(filter_criteria.approval_methods, query) + if filter_criteria.noc_statuses: + query = query.filter(Registration.noc_status.in_(filter_criteria.noc_statuses)) + if filter_criteria.is_set_aside is True: + query = query.filter(Registration.is_set_aside == True) # noqa: E712 sort_column = getattr(Registration, filter_criteria.sort_by, Registration.id) if filter_criteria.sort_order and filter_criteria.sort_order.lower() == "asc": query = query.order_by(sort_column.asc()) @@ -423,6 +429,31 @@ def _filter_by_registration_requirement(cls, requirement: list[str], query): query = query.filter(db.or_(*combined_conditions)) return query + @classmethod + def _filter_by_approval_method(cls, approval_methods: list[str], query): + """Filter registrations by application approval method. + + Only considers the most recent application (index 0 when sorted by + application_date desc) for each registration. Returns registrations + where that most recent application's status is in the given approval methods. + """ + if not approval_methods: + return query + # pylint: disable=import-outside-toplevel + from sqlalchemy import select + + from strr_api.models.application import Application + + # for each registration, get the status of the most recent application + latest_app_status_subq = ( + select(Application.status) + .where(Application.registration_id == Registration.id) + .order_by(Application.application_date.desc()) + .limit(1) + .scalar_subquery() + ) + return query.filter(latest_app_status_subq.in_(approval_methods)) + class RentalProperty(Versioned, BaseModel): """Rental Property""" diff --git a/strr-api/src/strr_api/resources/application.py b/strr-api/src/strr_api/resources/application.py index b676f067b..3b0d7b96e 100644 --- a/strr-api/src/strr_api/resources/application.py +++ b/strr-api/src/strr_api/resources/application.py @@ -264,6 +264,11 @@ def get_applications(): type: boolean default: true description: Include draft renewal applications + - in: query + name: applicationsOnly + type: boolean + default: false + description: When true, exclude applications that have a completed registration (except renewals). For split dashboard applications table. responses: 200: description: @@ -287,6 +292,7 @@ def get_applications(): requirements = request.args.getlist("requirement", None) include_draft_registration = request.args.get("includeDraftRegistration", "true").lower() == "true" include_draft_renewal = request.args.get("includeDraftRenewal", "true").lower() == "true" + applications_only = request.args.get("applicationsOnly", "false").lower() == "true" if sort_by not in VALID_SORT_FIELDS: sort_by = "id" if sort_order not in ["asc", "desc"]: @@ -304,6 +310,7 @@ def get_applications(): requirements=requirements, include_draft_registration=include_draft_registration, include_draft_renewal=include_draft_renewal, + applications_only=applications_only, ) application_list = ApplicationService.list_applications(account_id, filter_criteria=filter_criteria) return jsonify(application_list), HTTPStatus.OK diff --git a/strr-api/src/strr_api/resources/registrations.py b/strr-api/src/strr_api/resources/registrations.py index c3a001132..8ac7ee797 100644 --- a/strr-api/src/strr_api/resources/registrations.py +++ b/strr-api/src/strr_api/resources/registrations.py @@ -1015,6 +1015,24 @@ def search_registrations(): items: type: string description: Requirement filter (e.g., PR, BL, PROHIBITED, NO_REQ, PR_EXEMPT_STRATA_HOTEL, PR_EXEMPT_FARM_LAND, PR_EXEMPT_FRACTIONAL_OWNERSHIP, PLATFORM_MAJOR, PLATFORM_MEDIUM, PLATFORM_MINOR, STRATA_PR, STRATA_NO_PR). Can provide multiple values. + - in: query + name: approvalMethod + type: array + items: + type: string + enum: [FULL_REVIEW_APPROVED, AUTO_APPROVED, PROVISIONALLY_APPROVED] + description: Approval method filter. Can provide multiple values. + - in: query + name: nocStatus + type: array + items: + type: string + enum: [NOC_EXPIRED, NOC_PENDING] + description: NOC status filter. Can provide multiple values. + - in: query + name: isSetAside + type: boolean + description: Filter for set aside registrations when true. responses: 200: description: @@ -1034,6 +1052,10 @@ def search_registrations(): sort_order = request.args.get("sortOrder", "desc") assignee = request.args.get("assignee", None) requirements = request.args.getlist("requirement") or None + approval_methods = request.args.getlist("approvalMethod") or None + noc_statuses = request.args.getlist("nocStatus") or None + is_set_aside_param = request.args.get("isSetAside", None) + is_set_aside = is_set_aside_param.lower() == "true" if is_set_aside_param else None if sort_by not in VALID_REGISTRATION_SORT_FIELDS: sort_by = "id" if sort_order not in ["asc", "desc"]: @@ -1052,6 +1074,9 @@ def search_registrations(): sort_order=sort_order, assignee=assignee, requirements=requirements, + approval_methods=approval_methods, + noc_statuses=noc_statuses, + is_set_aside=is_set_aside, ) registration_list = RegistrationService.search_registrations(filter_criteria=filter_criteria) diff --git a/strr-api/src/strr_api/services/auth_service.py b/strr-api/src/strr_api/services/auth_service.py index f368c092b..479fb1397 100644 --- a/strr-api/src/strr_api/services/auth_service.py +++ b/strr-api/src/strr_api/services/auth_service.py @@ -50,13 +50,25 @@ class AuthService: """Wrapper service to interact with the Auth API.""" @classmethod - def get_service_client_token(cls): + def get_service_client_token( + cls, + client_id: str | None = None, + client_secret: str | None = None, + token_url: str | None = None, + timeout: int | None = None, + ): """Get service account client token for cross api calls.""" - client_id = current_app.config.get("STRR_SERVICE_ACCOUNT_CLIENT_ID") - client_secret = current_app.config.get("STRR_SERVICE_ACCOUNT_SECRET") - token_url = current_app.config.get("KEYCLOAK_AUTH_TOKEN_URL") - timeout = int(current_app.config.get("AUTH_SVC_TIMEOUT", 20)) + if not [ + client_id, + client_secret, + token_url, + timeout, + ]: + client_id = current_app.config.get("STRR_SERVICE_ACCOUNT_CLIENT_ID") + client_secret = current_app.config.get("STRR_SERVICE_ACCOUNT_SECRET") + token_url = current_app.config.get("KEYCLOAK_AUTH_TOKEN_URL") + timeout = int(current_app.config.get("AUTH_SVC_TIMEOUT", 20)) data = "grant_type=client_credentials" diff --git a/strr-api/tests/conftest.py b/strr-api/tests/conftest.py index 5f4ef723a..89cee8840 100644 --- a/strr-api/tests/conftest.py +++ b/strr-api/tests/conftest.py @@ -33,6 +33,7 @@ # POSSIBILITY OF SUCH DAMAGE. """Common setup and fixtures for the pytest suite used by this service.""" import contextlib +import os import random import string from contextlib import contextmanager @@ -40,6 +41,7 @@ import psycopg2 import pytest import sqlalchemy +from flask import g from flask_migrate import Migrate, upgrade from ldclient.integrations.test_data import TestData from sqlalchemy import event, text @@ -131,10 +133,21 @@ def ld(): yield td -@pytest.fixture(scope="session") -def client(app): # pylint: disable=redefined-outer-name - """Return a session-wide Flask test client.""" - return app.test_client() +# @pytest.fixture(scope="session") +# def client(app): # pylint: disable=redefined-outer-name +# """Return a session-wide Flask test client.""" +# return app.test_client() +@pytest.fixture(scope="function") +def client(app): + """ + Return a function-scoped test client. + This allows @patch decorators on individual tests to work correctly + without leaking state or context between tests. + """ + with app.test_client() as client: + # We provide the client, and the @patch decorators in your test + # will now correctly intercept calls made during client.get() + yield client @pytest.fixture(scope="session") @@ -143,6 +156,16 @@ def jwt(): return _jwt +@pytest.fixture +def authed_g(app): + """Fixture to seed 'g' with basic JWT info for tests that use mocks.""" + # This automatically uses the app context provided by the app fixture + with app.app_context(): + # Pre-seed the attribute that flask-jwt-oidc expects + setattr(g, "jwt_oidc_token_info", {"sub": "test-user", "realm_access": {"roles": []}}) + yield g + + @pytest.fixture(scope="session") def client_ctx(app): # pylint: disable=redefined-outer-name """Return session-wide Flask test client.""" @@ -161,30 +184,22 @@ def postgres_container(): @pytest.fixture(scope="session") def app(ld, postgres_container): - """ - Creates the Flask application using the container's credentials. - """ db_url = postgres_container.get_connection_url() Testing.SQLALCHEMY_DATABASE_URI = db_url - # This makes sure that the app is configured and doesn't skip setup steps Testing.POD_NAMESPACE = "Testing" - app = create_app(Testing, **{"ld_test_data": ld}) + # Still set this so the standalone-logic in env.py has a backup + os.environ["DATABASE_URL"] = db_url - with app.app_context(): - yield app + app = create_app(Testing, **{"ld_test_data": ld}) + return app @pytest.fixture(scope="session") def setup_database(app): - """ - Applies database migrations to the test container. - Replaces db.create_all() with flask_migrate.upgrade() - """ - # This applies all migrations up to 'head' - # It assumes your 'migrations' folder is in the project root - upgrade() - + # This is the ONLY place where the context is pushed for migrations + with app.app_context(): + upgrade() yield @@ -194,41 +209,33 @@ def session(app, setup_database): Creates a test session that behaves like a scoped_session but is bound to an external transaction for easy rollback. """ - # 1. Start the external transaction on the connection - connection = _db.engine.connect() - transaction = connection.begin() - - # 2. Create the Session - # join_transaction_mode="create_savepoint": - # This ensures that when your app calls session.commit(), it creates a - # nested SAVEPOINT (which we can rollback) instead of committing the real transaction. - session = AppSession(bind=connection, join_transaction_mode="create_savepoint") - - # 3. Create a Proxy to mimic Flask-SQLAlchemy's db.session - # This class ensures that both `db.session.add()` and `db.session()` work. - class TestScopedSession: - def __call__(self): - # Allows calling db.session() to get the current session - return session - - def __getattr__(self, name): - # Proxies attributes like .add, .query, .commit to the session - return getattr(session, name) - - def remove(self): - # Safe no-op or close - session.close() - - # 4. Patch global db.session - original_session_lookup = _db.session - _db.session = TestScopedSession() - - yield session - - # 5. Cleanup - _db.session = original_session_lookup # Restore global registry - session.close() - - # Force rollback of the external transaction (wiping all test data) - transaction.rollback() - connection.close() + with app.app_context(): # Re-establish context for the individual test + # 1. Start the external transaction on the connection + connection = _db.engine.connect() + transaction = connection.begin() + + # 2. Create the Session + session = AppSession(bind=connection, join_transaction_mode="create_savepoint") + + # 3. Create a Proxy to mimic Flask-SQLAlchemy's db.session + class TestScopedSession: + def __call__(self): + return session + + def __getattr__(self, name): + return getattr(session, name) + + def remove(self): + session.close() + + # 4. Patch global db.session + original_session_lookup = _db.session + _db.session = TestScopedSession() + + yield session + + # 5. Cleanup + session.close() + transaction.rollback() + connection.close() + _db.session = original_session_lookup diff --git a/strr-api/tests/unit/models/test_user.py b/strr-api/tests/unit/models/test_user.py index 9380c1e63..83d99cf41 100644 --- a/strr-api/tests/unit/models/test_user.py +++ b/strr-api/tests/unit/models/test_user.py @@ -85,7 +85,7 @@ def test_find_by_jwt_token_no_idp(): assert result is None -def test_get_or_create_user_by_jwt(): +def test_get_or_create_user_by_jwt(session): sample_token = { "iss": "test", "sub": "subTest", @@ -103,7 +103,7 @@ def test_create_from_jwt_token_no_token(): assert result is None -def test_get_or_create_user_by_jwt_no_user(): +def test_get_or_create_user_by_jwt_no_user(session): sample_token = { "iss": "x", "sub": "x", diff --git a/strr-api/tests/unit/resources/test_registration_applications.py b/strr-api/tests/unit/resources/test_registration_applications.py index bb4896b45..e7688f528 100644 --- a/strr-api/tests/unit/resources/test_registration_applications.py +++ b/strr-api/tests/unit/resources/test_registration_applications.py @@ -103,21 +103,21 @@ def test_staff_cannot_access_draft_applications(session, client, jwt): ], ) @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_create_host_registration_application(session, client, jwt, request_json): +def test_create_host_registration_application(app, session, client, jwt, request_json): with open(request_json) as f: json_data = json.load(f) headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID rv = client.post("/applications", json=json_data, headers=headers) - assert HTTPStatus.OK == rv.status_code - response_json = rv.json - assert response_json.get("header").get("hostStatus") == "Payment Due" - assert response_json.get("header").get("examinerStatus") == "Payment Due" - assert response_json.get("header").get("examinerActions") == [] - assert response_json.get("header").get("hostActions") == ApplicationSerializer.HOST_ACTIONS.get( - Application.Status.PAYMENT_DUE - ) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + assert response_json.get("header").get("hostStatus") == "Payment Due" + assert response_json.get("header").get("examinerStatus") == "Payment Due" + assert response_json.get("header").get("examinerActions") == [] + assert response_json.get("header").get("hostActions") == ApplicationSerializer.HOST_ACTIONS.get( + Application.Status.PAYMENT_DUE + ) def test_get_applications(session, client, jwt): @@ -340,7 +340,7 @@ def test_update_application_payment(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_reject_application(session, client, jwt): +def test_examiner_reject_application(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -381,7 +381,9 @@ def test_examiner_reject_application(session, client, jwt): ], ) @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_approve_host_registration_application(session, client, jwt, request_json, isUnitOnPrincipalResidence): +def test_examiner_approve_host_registration_application( + app, session, client, jwt, request_json, isUnitOnPrincipalResidence +): with open(request_json) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -420,7 +422,7 @@ def test_examiner_approve_host_registration_application(session, client, jwt, re @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_approve_platform_registration_application(session, client, jwt): +def test_examiner_approve_platform_registration_application(app, session, client, jwt): with open(CREATE_PLATFORM_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -626,7 +628,7 @@ def test_create_platform_registration_application(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_actions_for_application_in_full_review(session, client, jwt): +def test_actions_for_application_in_full_review(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -686,7 +688,7 @@ def test_create_registration_application_with_business_as_a_host(session, client @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_approve_registration_application_with_business_as_a_host(session, client, jwt): +def test_approve_registration_application_with_business_as_a_host(app, session, client, jwt): with open(CREATE_REGISTRATION_INDIVIDUAL_AS_COHOST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -721,7 +723,7 @@ def test_approve_registration_application_with_business_as_a_host(session, clien @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_approve_strata_hotel_registration_application(session, client, jwt): +def test_examiner_approve_strata_hotel_registration_application(app, session, client, jwt): with open(CREATE_STRATA_HOTEL_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -782,7 +784,7 @@ def test_save_and_resume_failed_for_paid_applications(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_create_business_as_cohost_registration(session, client, jwt): +def test_create_business_as_cohost_registration(app, session, client, jwt): with open(CREATE_REGISTRATION_BUSINESS_AS_COHOST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -930,7 +932,7 @@ def test_examiner_send_notice_of_consideration(mock_noc, mock_invoice, session, @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_filter_record_number_application(session, client, jwt): +def test_examiner_filter_record_number_application(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -984,6 +986,15 @@ def test_examiner_filter_record_number_application(session, client, jwt): assert rv.status_code == 200 assert len(response_json.get("applications")) == 0 + # Test applicationsOnly excludes approved registration applications (keeps renewals) + rv = client.get( + f"/applications?recordNumber={registration_number}&applicationsOnly=true", + headers=staff_headers, + ) + response_json = rv.json + assert rv.status_code == 200 + assert len(response_json.get("applications")) == 0 # Approved reg excluded when applicationsOnly=true + def test_examiner_multi_select_filters(session, client, jwt): staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") @@ -1038,7 +1049,7 @@ def test_examiner_multi_select_filters(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_assign_and_unassign_application(session, client, jwt): +def test_assign_and_unassign_application(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1200,7 +1211,7 @@ def test_send_notice_of_consideration_for_provisional_review(mock_noc, mock_invo @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_decline_application_registration_provisional_review(session, client, jwt): +def test_examiner_decline_application_registration_provisional_review(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1237,7 +1248,7 @@ def test_examiner_decline_application_registration_provisional_review(session, c @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_set_aside_application_refusal_decision(session, client, jwt): +def test_examiner_set_aside_application_refusal_decision(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1278,7 +1289,7 @@ def test_examiner_set_aside_application_refusal_decision(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_refuse_application_after_set_aside(session, client, jwt): +def test_examiner_refuse_application_after_set_aside(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1334,7 +1345,7 @@ def test_examiner_refuse_application_after_set_aside(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_approve_application_after_set_aside(session, client, jwt): +def test_examiner_approve_application_after_set_aside(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID diff --git a/strr-api/tests/unit/resources/test_registrations.py b/strr-api/tests/unit/resources/test_registrations.py index 14d73c3f3..3d650545d 100644 --- a/strr-api/tests/unit/resources/test_registrations.py +++ b/strr-api/tests/unit/resources/test_registrations.py @@ -1,7 +1,7 @@ import json import os import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from http import HTTPStatus from unittest.mock import patch @@ -55,7 +55,7 @@ MOCK_DOCUMENT_UPLOAD = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../mocks/file/document_upload.txt") -def test_get_registrations_200(session, client, jwt): +def test_get_registrations_200(app, session, client, jwt): headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID rv = client.get("/registrations", headers=headers) @@ -73,7 +73,8 @@ def test_get_registrations_401(client): @patch("strr_api.services.registration_service.RegistrationService.get_registration", new=fake_registration) @patch("strr_api.services.document_service.DocumentService.get_registration_document_by_key") @patch("strr_api.services.document_service.DocumentService.get_file_by_key") -def test_get_registration_file_by_id_200(mock_get_file, mock_get_document, client): +def test_get_registration_file_by_id_200(mock_get_file, mock_get_document, client, authed_g): + # Manually seed the context that flask-jwt-oidc is looking for mock_document = fake_document() mock_document.file_name = "test.pdf" mock_document.file_type = "application/pdf" @@ -97,7 +98,7 @@ def test_get_registration_file_by_id_401(client): @patch("flask_jwt_oidc.JwtManager.get_token_auth_header", new=fake_get_token_auth_header) @patch("flask_jwt_oidc.JwtManager._validate_token", new=no_op) @patch("strr_api.services.registration_service.RegistrationService.get_registration", return_value=None) -def test_get_registration_file_by_id_403(mock_get_registration, client): +def test_get_registration_file_by_id_403(mock_get_registration, client, authed_g): rv = client.get("/registrations/1/documents/test-key") assert rv.status_code == HTTPStatus.FORBIDDEN @@ -107,7 +108,7 @@ def test_get_registration_file_by_id_403(mock_get_registration, client): @patch("flask_jwt_oidc.JwtManager._validate_token", new=no_op) @patch("strr_api.services.registration_service.RegistrationService.get_registration", new=fake_registration) @patch("strr_api.services.document_service.DocumentService.get_registration_document_by_key", return_value=None) -def test_get_registration_file_by_id_404(mock_get_document, client): +def test_get_registration_file_by_id_404(mock_get_document, client, authed_g): rv = client.get("/registrations/1/documents/test-key") assert rv.status_code == HTTPStatus.NOT_FOUND @@ -121,7 +122,7 @@ def test_get_registration_file_by_id_404(mock_get_document, client): "strr_api.services.document_service.DocumentService.get_file_by_key", side_effect=ExternalServiceException("External service error"), ) -def test_get_registration_file_by_id_502(mock_get_file, mock_get_document, client): +def test_get_registration_file_by_id_502(mock_get_file, mock_get_document, client, authed_g): mock_document = fake_document() mock_get_document.return_value = mock_document rv = client.get("/registrations/1/documents/test-key") @@ -129,7 +130,7 @@ def test_get_registration_file_by_id_502(mock_get_file, mock_get_document, clien @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_get_registration_events(session, client, jwt): +def test_get_registration_events(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -173,7 +174,7 @@ def test_get_registration_events(session, client, jwt): @pytest.mark.skip(reason="Skipping the test until certificate generation is supported") @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_issue_certificate_for_host_registration(session, client, jwt): +def test_examiner_issue_certificate_for_host_registration(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -220,7 +221,7 @@ def test_examiner_issue_certificate_for_host_registration(session, client, jwt): @pytest.mark.skip(reason="Skipping the test until certificate generation is supported") @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_examiner_issue_certificate_for_platform_registration(session, client, jwt): +def test_examiner_issue_certificate_for_platform_registration(app, session, client, jwt): with open(CREATE_PLATFORM_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -252,7 +253,7 @@ def test_examiner_issue_certificate_for_platform_registration(session, client, j @pytest.mark.skip(reason="Skipping the test until certificate generation is supported") @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_issue_certificate_public_user(session, client, jwt): +def test_issue_certificate_public_user(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -284,7 +285,7 @@ def test_issue_certificate_public_user(session, client, jwt): @pytest.mark.skip(reason="Skipping the test until certificate generation is supported") @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_get_registration_certificate(session, client, jwt): +def test_get_registration_certificate(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -325,7 +326,7 @@ def test_get_registration_certificate_401(client): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_get_host_registration_by_id(session, client, jwt): +def test_get_host_registration_by_id(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -357,7 +358,7 @@ def test_get_host_registration_by_id(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_get_platform_registration_by_id(session, client, jwt): +def test_get_platform_registration_by_id(app, session, client, jwt): with open(CREATE_PLATFORM_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -394,7 +395,7 @@ def test_get_registration_by_id_unauthorized(client): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_get_registration_by_id_includes_document_added_on(session, client, jwt): +def test_get_registration_by_id_includes_document_added_on(app, session, client, jwt): """GET /registrations/ returns documents with addedOn (from DB created when added_on is null).""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -485,7 +486,7 @@ def test_registration_serializer_document_added_on_uses_created_when_added_on_nu @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_cancel_registration(session, client, jwt): +def test_cancel_registration(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -521,7 +522,7 @@ def test_cancel_registration(session, client, jwt): assert response_json.get("cancelledDate") is not None -def test_get_expired_registration_todos_in_renewal_window(session, client, jwt): +def test_get_expired_registration_todos_in_renewal_window(app, session, client, jwt): headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -553,7 +554,7 @@ def test_get_expired_registration_todos_in_renewal_window(session, client, jwt): assert response_json.get("todos")[0].get("task") is not None -def test_get_expired_registration_todos_outside_renewal_window(session, client, jwt): +def test_get_expired_registration_todos_outside_renewal_window(app, session, client, jwt): headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -585,7 +586,7 @@ def test_get_expired_registration_todos_outside_renewal_window(session, client, assert response_json.get("todos") == [] -def test_get_active_registration_todos_in_renewal_window(session, client, jwt): +def test_get_active_registration_todos_in_renewal_window(app, session, client, jwt): headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -617,7 +618,7 @@ def test_get_active_registration_todos_in_renewal_window(session, client, jwt): assert response_json.get("todos")[0].get("task") is not None -def test_get_active_registration_todos_outside_renewal_window(session, client, jwt): +def test_get_active_registration_todos_outside_renewal_window(app, session, client, jwt): headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -649,7 +650,7 @@ def test_get_active_registration_todos_outside_renewal_window(session, client, j assert response_json.get("todos") == [] -def test_get_todos_with_renewal_states(session, client, jwt): +def test_get_todos_with_renewal_states(app, session, client, jwt): """Test renewal todos with draft, payment due status and default.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -722,7 +723,7 @@ def test_get_todos_with_renewal_states(session, client, jwt): assert todos[0].get("task").get("detail") == payment_due_application.application_number -def test_get_todos_with_submitted_renewal_recent_and_old(session, client, jwt): +def test_get_todos_with_submitted_renewal_recent_and_old(app, session, client, jwt): """Test renewal todos if renewal application was submitted.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -781,7 +782,7 @@ def test_get_todos_with_submitted_renewal_recent_and_old(session, client, jwt): "strr_api.services.approval_service.ApprovalService.getSTRDataForAddress", return_value={"organizationNm": "Test Municipality"}, ) -def test_update_registration_str_address(mock_get_str_data, session, client, jwt): +def test_update_registration_str_address(mock_get_str_data, app, session, client, jwt): """Test updating the STR address for a registration.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -851,7 +852,7 @@ def test_update_registration_str_address(mock_get_str_data, session, client, jwt @patch( "strr_api.services.approval_service.ApprovalService.getSTRDataForAddress", side_effect=Exception("Service error") ) -def test_update_registration_str_address_service_error_with_jurisdiction(mock_get_str_data, session, client, jwt): +def test_update_registration_str_address_service_error_with_jurisdiction(mock_get_str_data, app, session, client, jwt): """Test updating STR address when getSTRDataForAddress returns an error.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -897,7 +898,7 @@ def test_update_registration_str_address_service_error_with_jurisdiction(mock_ge @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) @patch("strr_api.services.approval_service.ApprovalService.getSTRDataForAddress", return_value={"organizationNm": ""}) -def test_update_registration_str_address_empty_jurisdiction(mock_get_str_data, session, client, jwt): +def test_update_registration_str_address_empty_jurisdiction(mock_get_str_data, app, session, client, jwt): """Test updating STR address when getSTRDataForAddress returns empty jurisdiction.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -942,7 +943,7 @@ def test_update_registration_str_address_empty_jurisdiction(mock_get_str_data, s @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_update_registration_str_address_with_jurisdiction(session, client, jwt): +def test_update_registration_str_address_with_jurisdiction(app, session, client, jwt): """Test updating STR address when jurisdiction is provided in unit address.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -996,7 +997,7 @@ def test_update_registration_str_address_with_jurisdiction(session, client, jwt) @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_set_aside_registration_decision(session, client, jwt): +def test_set_aside_registration_decision(app, session, client, jwt): """Test setting aside a registration decision.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1039,7 +1040,7 @@ def test_set_aside_registration_decision(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_reinstate_registration_using_status_endpoint(session, client, jwt): +def test_reinstate_registration_using_status_endpoint(app, session, client, jwt): """Test reinstating a registration using the /status endpoint after set-aside.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1076,7 +1077,7 @@ def test_reinstate_registration_using_status_endpoint(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_set_aside_registration_events(session, client, jwt): +def test_set_aside_registration_events(app, session, client, jwt): """Test that set-aside registration creates proper events.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1138,7 +1139,7 @@ def test_set_aside_registration_events(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_cancel_registration_using_status_endpoint(session, client, jwt): +def test_cancel_registration_using_status_endpoint(app, session, client, jwt): """Test canceling a registration using the /status endpoint after set-aside.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1176,7 +1177,7 @@ def test_cancel_registration_using_status_endpoint(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_upload_registration_document(session, client, jwt): +def test_upload_registration_document(app, session, client, jwt): """Test successful document upload to registration with NOC_PENDING status.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1416,7 +1417,7 @@ def test_send_notice_of_consideration_validation_errors(mock_invoice, session, c @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_assign_and_unassign_registration(session, client, jwt): +def test_assign_and_unassign_registration(app, session, client, jwt): """Test assigning and unassigning a registration to a staff user.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1470,7 +1471,7 @@ def test_assign_and_unassign_registration(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_registration_status_update_sets_decider_id(session, client, jwt): +def test_registration_status_update_sets_decider_id(app, session, client, jwt): """Test that updating registration status sets the decider_id field.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1515,7 +1516,7 @@ def test_registration_status_update_sets_decider_id(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_application_status_update_sets_decider_id(session, client, jwt): +def test_application_status_update_sets_decider_id(app, session, client, jwt): """Test that updating application status sets the decider_id field.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1563,7 +1564,7 @@ def test_application_status_update_sets_decider_id(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_add_conditions_on_registration(session, client, jwt): +def test_add_conditions_on_registration(app, session, client, jwt): """Test reinstating a registration using the /status endpoint after set-aside.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1607,7 +1608,7 @@ def test_add_conditions_on_registration(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_clear_conditions_on_registration(session, client, jwt): +def test_clear_conditions_on_registration(app, session, client, jwt): """Test reinstating a registration using the /status endpoint after set-aside.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1660,7 +1661,7 @@ def test_clear_conditions_on_registration(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_clear_predefined_conditions(session, client, jwt): +def test_clear_predefined_conditions(app, session, client, jwt): """Test reinstating a registration using the /status endpoint after set-aside.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") @@ -1721,7 +1722,7 @@ def test_clear_predefined_conditions(session, client, jwt): assert response_json.get("conditionsOfApproval").get("customConditions") == ["Condition 1", "Condition 2"] -def test_strata_hotel_todos_60_day_renewal_window(session, client, jwt): +def test_strata_hotel_todos_60_day_renewal_window(app, session, client, jwt): """Test that strata hotels show renewal todo 60 days before expiry.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1755,7 +1756,7 @@ def test_strata_hotel_todos_60_day_renewal_window(session, client, jwt): assert response_json.get("todos")[0].get("task").get("type") == "REGISTRATION_RENEWAL" -def test_strata_hotel_todos_outside_60_day_renewal_window(session, client, jwt): +def test_strata_hotel_todos_outside_60_day_renewal_window(app, session, client, jwt): """Test that strata hotels does not show renewal todo outside 60-day window.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1788,7 +1789,7 @@ def test_strata_hotel_todos_outside_60_day_renewal_window(session, client, jwt): assert response_json.get("todos") == [] -def test_host_todos_40_day_renewal_window(session, client, jwt): +def test_host_todos_40_day_renewal_window(app, session, client, jwt): """Test that host registrations still use 40-day window.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1821,7 +1822,7 @@ def test_host_todos_40_day_renewal_window(session, client, jwt): assert response_json.get("todos") == [] -def test_platform_todos_40_day_renewal_window(session, client, jwt): +def test_platform_todos_40_day_renewal_window(app, session, client, jwt): """Test that platform registrations still use 40-day window.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1854,7 +1855,7 @@ def test_platform_todos_40_day_renewal_window(session, client, jwt): assert response_json.get("todos") == [] -def test_get_platform_todos_with_all_renewal_states(session, client, jwt): +def test_get_platform_todos_with_all_renewal_states(app, session, client, jwt): """Test platform renewal todos - Renewal, Draft, Payment Due.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1924,7 +1925,7 @@ def test_get_platform_todos_with_all_renewal_states(session, client, jwt): assert todos[0].get("task").get("detail") == payment_due_application.application_number -def test_get_strata_todos_with_all_renewal_states(session, client, jwt): +def test_get_strata_todos_with_all_renewal_states(app, session, client, jwt): """Test platform renewal todos - Renewal, Draft, Payment Due.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -1995,7 +1996,7 @@ def test_get_strata_todos_with_all_renewal_states(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations(session, client, jwt): +def test_search_registrations(app, session, client, jwt): """Test examiner search registrations endpoint.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2042,7 +2043,7 @@ def test_search_registrations_401(client): assert rv.status_code == HTTPStatus.UNAUTHORIZED -def test_search_registrations_short_search_text(session, client, jwt): +def test_search_registrations_short_search_text(app, session, client, jwt): """Test examiner search registrations with search text less than 3 characters.""" staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") rv = client.get("/registrations/search?text=ab", headers=staff_headers) @@ -2050,7 +2051,7 @@ def test_search_registrations_short_search_text(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_user_search_registrations(session, client, jwt): +def test_user_search_registrations(app, session, client, jwt): """Test user search registrations endpoint.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2099,14 +2100,14 @@ def test_user_search_registrations_401(client): assert rv.status_code == HTTPStatus.UNAUTHORIZED -def test_user_search_registrations_missing_account_id(session, client, jwt): +def test_user_search_registrations_missing_account_id(app, session, client, jwt): """Test user search registrations endpoint returns 400 without Account-Id header.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") rv = client.get("/registrations/user/search", headers=headers) assert rv.status_code == HTTPStatus.BAD_REQUEST -def test_user_search_registrations_short_search_text(session, client, jwt): +def test_user_search_registrations_short_search_text(app, session, client, jwt): """Test user search registrations with search text less than 3 characters.""" headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -2115,7 +2116,7 @@ def test_user_search_registrations_short_search_text(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_by_single_requirement(session, client, jwt): +def test_search_registrations_by_single_requirement(app, session, client, jwt): """Test examiner search registrations filtered by single requirement.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2151,7 +2152,7 @@ def test_search_registrations_by_single_requirement(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_by_multiple_requirements(session, client, jwt): +def test_search_registrations_by_multiple_requirements(app, session, client, jwt): """Test examiner search registrations filtered by single requirement (not multiple with AND logic).""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2187,7 +2188,7 @@ def test_search_registrations_by_multiple_requirements(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_requirement_and_status_combined(session, client, jwt): +def test_search_registrations_requirement_and_status_combined(app, session, client, jwt): """Test search registrations with both requirement and status filters combined.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2226,7 +2227,7 @@ def test_search_registrations_requirement_and_status_combined(session, client, j @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_requirement_no_requirement(session, client, jwt): +def test_search_registrations_requirement_no_requirement(app, session, client, jwt): """Test search registrations for NO_REQ (no requirements) hosts.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2258,7 +2259,7 @@ def test_search_registrations_requirement_no_requirement(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_with_registration_type_and_requirement(session, client, jwt): +def test_search_registrations_with_registration_type_and_requirement(app, session, client, jwt): """Test search registrations with both registration type and requirement filters.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2297,7 +2298,7 @@ def test_search_registrations_with_registration_type_and_requirement(session, cl @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_by_platform_requirement(session, client, jwt): +def test_search_registrations_by_platform_requirement(app, session, client, jwt): """Test search registrations for platform registrations with PLATFORM_MAJOR requirement.""" with open(CREATE_PLATFORM_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2333,7 +2334,7 @@ def test_search_registrations_by_platform_requirement(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_by_strata_requirement(session, client, jwt): +def test_search_registrations_by_strata_requirement(app, session, client, jwt): """Test search registrations for strata registrations with STRATA_PR requirement.""" # Note: This test would require creating a strata hotel registration # For now, we'll test the endpoint accepts the parameter correctly @@ -2348,7 +2349,7 @@ def test_search_registrations_by_strata_requirement(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_requirement_with_pagination(session, client, jwt): +def test_search_registrations_requirement_with_pagination(app, session, client, jwt): """Test search registrations with requirement filter and pagination.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2381,7 +2382,7 @@ def test_search_registrations_requirement_with_pagination(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_search_registrations_requirement_with_sorting(session, client, jwt): +def test_search_registrations_requirement_with_sorting(app, session, client, jwt): """Test search registrations with requirement filter and sorting.""" with open(CREATE_HOST_REGISTRATION_REQUEST) as f: json_data = json.load(f) @@ -2412,3 +2413,147 @@ def test_search_registrations_requirement_with_sorting(session, client, jwt): registrations = rv.json assert "registrations" in registrations assert len(registrations.get("registrations")) >= 0 + + +@patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) +def test_search_registrations_with_approval_method_noc_status_set_aside_filters(app, session, client, jwt): + """Test search registrations with approvalMethod, nocStatus, and isSetAside filters.""" + with open(CREATE_HOST_REGISTRATION_REQUEST) as f: + json_data = json.load(f) + headers = create_header(jwt, [PUBLIC_USER], "Account-Id") + headers["Account-Id"] = ACCOUNT_ID + rv = client.post("/applications", json=json_data, headers=headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + application_number = response_json.get("header").get("applicationNumber") + + application = Application.find_by_application_number(application_number=application_number) + application.payment_status = PaymentStatus.COMPLETED.value + application.status = Application.Status.FULL_REVIEW + application.save() + + staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") + rv = client.put(f"/applications/{application_number}/assign", headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + status_update_request = {"status": Application.Status.FULL_REVIEW_APPROVED} + rv = client.put(f"/applications/{application_number}/status", json=status_update_request, headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + registration_number = response_json.get("header").get("registrationNumber") + + # Search by approval method - FULL_REVIEW_APPROVED + rv = client.get( + f"/registrations/search?approvalMethod=FULL_REVIEW_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + # Search by nocStatus - create registration with NOC_PENDING and verify filter + registration = Registration.query.filter_by(registration_number=registration_number).one_or_none() + registration.noc_status = RegistrationNocStatus.NOC_PENDING.value + registration.save() + + rv = client.get( + f"/registrations/search?nocStatus=NOC_PENDING&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + # Search by isSetAside - set aside the registration and verify filter + registration.is_set_aside = True + registration.save() + + rv = client.get( + f"/registrations/search?isSetAside=true&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + assert len(registrations.get("registrations")) >= 1 + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert found + + +@patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) +def test_search_registrations_approval_method_uses_most_recent_application_only( + mock_create_invoice, session, client, jwt +): + """Test that approvalMethod filter considers only the most recent application (index 0). + + When a registration has multiple applications (e.g. initial FULL_REVIEW_APPROVED, + renewal PROVISIONALLY_APPROVED), the filter should use only the most recent one. + So filtering for FULL_REVIEW_APPROVED should NOT return such a registration. + """ + from nanoid import generate + + with open(CREATE_HOST_REGISTRATION_REQUEST) as f: + json_data = json.load(f) + headers = create_header(jwt, [PUBLIC_USER], "Account-Id") + headers["Account-Id"] = ACCOUNT_ID + rv = client.post("/applications", json=json_data, headers=headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + application_number = response_json.get("header").get("applicationNumber") + + application = Application.find_by_application_number(application_number=application_number) + application.payment_status = PaymentStatus.COMPLETED.value + application.status = Application.Status.FULL_REVIEW + application.save() + + staff_headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") + rv = client.put(f"/applications/{application_number}/assign", headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + status_update_request = {"status": Application.Status.FULL_REVIEW_APPROVED} + rv = client.put(f"/applications/{application_number}/status", json=status_update_request, headers=staff_headers) + assert HTTPStatus.OK == rv.status_code + response_json = rv.json + registration_number = response_json.get("header").get("registrationNumber") + registration = Registration.query.filter_by(registration_number=registration_number).one_or_none() + + # Refresh application to get its application_date from DB, then add a renewal that is + # explicitly newer (guarantees correct ordering regardless of test execution timing) + session.refresh(application) + renewal_app = Application( + application_json=application.application_json, + application_number=generate(alphabet="0123456789", size=14), + type=ApplicationType.RENEWAL.value, + registration_type=application.registration_type, + status=Application.Status.PROVISIONALLY_APPROVED, + registration_id=registration.id, + application_date=application.application_date + timedelta(seconds=1), + ) + session.add(renewal_app) + session.commit() + + # Filter for FULL_REVIEW_APPROVED - should NOT return this registration + # (most recent app is PROVISIONALLY_APPROVED) + rv = client.get( + f"/registrations/search?approvalMethod=FULL_REVIEW_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert ( + not found + ), "Registration with most recent app PROVISIONALLY_APPROVED should not match FULL_REVIEW_APPROVED" + + # Filter for PROVISIONALLY_APPROVED - should return this registration + rv = client.get( + f"/registrations/search?approvalMethod=PROVISIONALLY_APPROVED&status={RegistrationStatus.ACTIVE.value}", + headers=staff_headers, + ) + assert rv.status_code == HTTPStatus.OK + registrations = rv.json + found = any(r.get("registrationNumber") == registration_number for r in registrations.get("registrations")) + assert ( + found + ), "Registration with most recent app PROVISIONALLY_APPROVED should match PROVISIONALLY_APPROVED filter" diff --git a/strr-api/tests/unit/resources/test_renewal_applications.py b/strr-api/tests/unit/resources/test_renewal_applications.py index 36069efa1..c6f28b59d 100644 --- a/strr-api/tests/unit/resources/test_renewal_applications.py +++ b/strr-api/tests/unit/resources/test_renewal_applications.py @@ -85,12 +85,22 @@ def test_host_renewal_application_submission(session, client, jwt, request_json, json_data["header"] = renewal_header_json rv = client.post("/applications", json=json_data, headers=headers) response_json = rv.json - application_number = response_json.get("header").get("applicationNumber") - assert application_number is not None - application = Application.find_by_application_number(application_number=application_number) + renewal_application_number = response_json.get("header").get("applicationNumber") + assert renewal_application_number is not None + application = Application.find_by_application_number(application_number=renewal_application_number) assert application.registration_id == registration_id assert application.type == "renewal" + # Test applicationsOnly includes renewal applications (renewals have registration_id but are shown) + rv = client.get( + f"/applications?recordNumber={renewal_application_number}&applicationsOnly=true", + headers=staff_headers, + ) + response_json = rv.json + assert rv.status_code == 200 + assert len(response_json.get("applications")) == 1 + assert response_json.get("applications")[0]["header"]["applicationNumber"] == renewal_application_number + @pytest.mark.parametrize( "request_json, isUnitOnPrincipalResidence", diff --git a/strr-api/tests/unit/resources/test_validation.py b/strr-api/tests/unit/resources/test_validation.py index b26a186ec..0ea9a22d0 100644 --- a/strr-api/tests/unit/resources/test_validation.py +++ b/strr-api/tests/unit/resources/test_validation.py @@ -23,7 +23,7 @@ } -def test_permit_does_not_exist(session, client, jwt): +def test_permit_does_not_exist(app, session, client, jwt): headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") validate_permit_request = {"identifier": "H123", "address": {"streetNumber": "2435", "postalCode": "V4A 8H4"}} rv = client.post("/permits/:validatePermit", json=validate_permit_request, headers=headers) @@ -42,7 +42,7 @@ def test_permit_does_not_exist(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_permit_exists(session, client, jwt): +def test_permit_exists(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -86,7 +86,7 @@ def test_permit_exists(session, client, jwt): @patch("strr_api.services.strr_pay.create_invoice", return_value=MOCK_INVOICE_RESPONSE) -def test_permit_details_mismatch(session, client, jwt): +def test_permit_details_mismatch(app, session, client, jwt): with open(CREATE_HOST_REGISTRATION_REQUEST) as f: headers = create_header(jwt, [PUBLIC_USER], "Account-Id") headers["Account-Id"] = ACCOUNT_ID @@ -134,7 +134,7 @@ def test_permit_details_mismatch(session, client, jwt): ) -def test_invalid_request_with_identifier(session, client, jwt): +def test_invalid_request_with_identifier(app, session, client, jwt): headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") validate_permit_request = {"identifier": "H123", "address": {"postalCode": "V4A 8H4"}} rv = client.post("/permits/:validatePermit", json=validate_permit_request, headers=headers) @@ -150,7 +150,7 @@ def test_invalid_request_with_identifier(session, client, jwt): assert response_json.get("errors")[0].get("message") == "'streetNumber' is a required property" -def test_invalid_request_without_identifier(session, client, jwt): +def test_invalid_request_without_identifier(app, session, client, jwt): headers = create_header(jwt, [STRR_EXAMINER], "Account-Id") validate_permit_request = {"address": {"streetNumber": "2435", "postalCode": "V4A 8H4"}} rv = client.post("/permits/:validatePermit", json=validate_permit_request, headers=headers) diff --git a/strr-api/tests/unit/services/test_interaction_service.py b/strr-api/tests/unit/services/test_interaction_service.py index 3a808397b..997e00af3 100644 --- a/strr-api/tests/unit/services/test_interaction_service.py +++ b/strr-api/tests/unit/services/test_interaction_service.py @@ -170,7 +170,7 @@ def test_dispatch_email_interaction_success(mock_requests_post, mock_get_token, @patch("strr_api.services.auth_service.AuthService.get_service_client_token", return_value="dummy_token") @patch("strr_api.services.interaction.requests.post") def test_dispatch_email_interaction_failure_zero_id( - mock_requests_post, mock_get_token, session, setup_parents, inject_config + mock_requests_post, mock_get_token, app, session, setup_parents, inject_config ): """Assert that email interaction fails when notify_reference id is 0.""" mock_requests_post.return_value.status_code = HTTPStatus.OK @@ -195,7 +195,7 @@ def test_dispatch_email_interaction_failure_zero_id( @patch("strr_api.services.auth_service.AuthService.get_service_client_token", return_value="dummy_token") @patch("strr_api.services.interaction.requests.post") def test_dispatch_email_interaction_failure_none_id( - mock_requests_post, mock_get_token, session, setup_parents, inject_config + mock_requests_post, mock_get_token, session, setup_parents, inject_config, authed_g ): """Assert that email interaction fails when notify_reference is None.""" mock_requests_post.return_value.status_code = HTTPStatus.BAD_REQUEST diff --git a/strr-examiner-web/app/components/Table/Header/Select.vue b/strr-examiner-web/app/components/Table/Header/Select.vue index c32d76dd3..98df31232 100644 --- a/strr-examiner-web/app/components/Table/Header/Select.vue +++ b/strr-examiner-web/app/components/Table/Header/Select.vue @@ -67,7 +67,13 @@ watch(filterModel, (newVal, oldVal) => { if (parentChecked) { childStatuses!.forEach(status => result.add(status)) // check all child statuses } else if (parentUnchecked) { - childStatuses!.forEach(status => result.delete(status)) // uncheck all child statuses + // cascade the uncheck to children only if all are still selected (user toggled parent off) + // skip cascade if any child is already gone, because of external change (clear filter button) + const allChildrenStillPresent = childStatuses!.every(status => result.has(status)) + // this check distinguishes between the two cases so the watcher doesn't fight with external resets + if (allChildrenStillPresent) { + childStatuses!.forEach(status => result.delete(status)) // uncheck all child statuses + } } else if (allChildrenSelected) { result.add(parentVal) // if all children checked - auto-check parent } else { @@ -165,7 +171,7 @@ onMounted(() => {