Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .sonarcloud.properties
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ sonar.projectVersion=Autoscan

# Path to sources
#sonar.sources=strr-api/src/**/*
sonar.exclusions=strr-api/migrations/**/*,strr-api/devops/**/*,strr-api/tests/**/*,testing/**/*
sonar.exclusions=strr-api/migrations/**/*,strr-api/devops/**/*,strr-api/tests/**/*,testing/**/*,queue_services/**/tests/**/*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whats the reason for adding this?

#sonar.inclusions=

# Path to tests
Expand Down
780 changes: 778 additions & 2 deletions queue_services/strr-email/poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions queue_services/strr-email/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ isort = "^5.13.2"
pylint = "^3.0.3"
gcp-queue = { git = "https://github.com/bcgov/sbc-connect-common.git", subdirectory = "python/gcp-queue", branch = "main" }
structured-logging = { git = "https://github.com/bcgov/sbc-connect-common.git", subdirectory = "python/structured-logging", branch = "main" }
cloud-sql-connector = { git = "https://github.com/bcgov/sbc-connect-common.git", subdirectory = "python/cloud-sql-connector", branch = "main" }
strr-api = {git = "https://github.com/bcgov/STRR.git", rev = "main", subdirectory = "strr-api"}
nanoid = "^2.0.0"
# strr-api = {path = "/workspaces/STRR/strr-api", develop = true}
Expand Down
4 changes: 4 additions & 0 deletions queue_services/strr-email/src/strr_email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"""The email queue listener service."""
from __future__ import annotations

from cloud_sql_connector import setup_pg8000_close_event_listener
from flask import Flask
from flask_migrate import Migrate
import sentry_sdk
Expand Down Expand Up @@ -63,6 +64,9 @@ def create_app(environment: Config = ProdConfig, **_kwargs) -> Flask:
if app.config.get("POD_NAMESPACE", None) == "Testing":
Migrate(app, db)

with app.app_context():
setup_pg8000_close_event_listener(db.engine)

gcp_queue.init_app(app)
register_endpoints(app)

Expand Down
118 changes: 118 additions & 0 deletions queue_services/strr-email/tests/integration/test_queue_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,3 +379,121 @@ def notify_callback(request):
assert stored.registration_id == registration.id
assert stored.status == InteractionStatus.SENT
assert stored.meta_data["email_type"] == "STRATA_HOTEL_REGISTRATION_ACTIVE"


def test_email_no_cloud_event_data(client, queue_envelope, simple_cloud_event):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are these unit & integration tests added for in this project?

wasnt it already at 100% coverage?

"""Cloud event with no data is a no-op returning 200."""
ce = simple_cloud_event(type="email", data={})
envelope = queue_envelope(cloud_event=ce)
response = client.post("/", json=envelope)
assert response.status_code == HTTPStatus.OK


def test_email_application_not_found(client, session, queue_envelope, simple_cloud_event):
"""Returns 404 when the application_number in the event does not exist."""
data = {
"email_type": "HOST_AUTO_APPROVED",
"application_number": "NONEXISTENT-APP-12345",
}
ce = simple_cloud_event(type="email", data=data)
envelope = queue_envelope(cloud_event=ce)
response = client.post("/", json=envelope)
assert response.status_code == HTTPStatus.NOT_FOUND


def test_email_registration_not_found(client, session, queue_envelope, simple_cloud_event):
"""Returns 404 when the registration_number in the event does not exist."""
data = {
"email_type": "HOST_RENEWAL_REMINDER",
"registration_number": "NONEXISTENT-REG-99999",
}
ce = simple_cloud_event(type="email", data=data)
envelope = queue_envelope(cloud_event=ce)
response = client.post("/", json=envelope)
assert response.status_code == HTTPStatus.NOT_FOUND


@pytest.mark.conf(
KEYCLOAK_AUTH_TOKEN_URL="http://my-auth-url",
NOTIFY_SVC_URL="http://my-notify-mock",
NOTIFY_API_TIMEOUT=30,
EMAIL_HOUSING_RECIPIENT_EMAIL="[email protected]",
)
@responses.activate
def test_email_notify_api_failure(
app,
client,
session,
simple_cloud_event,
queue_envelope,
setup_parents,
inject_config,
):
"""Returns an error status when the notify API call fails."""
responses.add(
responses.POST,
app.config.get("KEYCLOAK_AUTH_TOKEN_URL"),
json={"access_token": "token"},
status=200,
)
responses.add(
responses.POST,
app.config.get("NOTIFY_SVC_URL"),
json={"message": "Service unavailable"},
status=503,
)

registration = create_registration(session, setup_parents)

data = {
"email_type": "HOST_REGISTRATION_CANCELLED",
"registration_number": registration.registration_number,
}
ce = simple_cloud_event(type="email", data=data)
envelope = queue_envelope(cloud_event=ce)

response = client.post("/", json=envelope)
assert response.status_code == HTTPStatus.BAD_REQUEST


@pytest.mark.conf(
KEYCLOAK_AUTH_TOKEN_URL="http://my-auth-url",
NOTIFY_SVC_URL="http://my-notify-mock",
NOTIFY_API_TIMEOUT=30,
EMAIL_HOUSING_RECIPIENT_EMAIL="[email protected]",
)
@responses.activate
def test_email_host_registration_cancelled(
app,
client,
session,
simple_cloud_event,
queue_envelope,
setup_parents,
inject_config,
):
"""Returns 200 and calls notify-api directly for non-renewal email types."""
responses.add(
responses.POST,
app.config.get("KEYCLOAK_AUTH_TOKEN_URL"),
json={"access_token": "token"},
status=200,
)
responses.add(
responses.POST,
app.config.get("NOTIFY_SVC_URL"),
json={"id": "notif-123"},
status=200,
)

registration = create_registration(session, setup_parents)

data = {
"email_type": "HOST_REGISTRATION_CANCELLED",
"registration_number": registration.registration_number,
}
ce = simple_cloud_event(type="email", data=data)
envelope = queue_envelope(cloud_event=ce)

response = client.post("/", json=envelope)
assert response.status_code == HTTPStatus.OK
207 changes: 207 additions & 0 deletions queue_services/strr-email/tests/unit/test_email_listener_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
"""Unit tests for email_listener utility functions (no Flask or DB required)."""

import pytest
from simple_cloudevent import SimpleCloudEvent
from strr_api.models import Registration

from strr_email.resources.email_listener import _get_address_detail
from strr_email.resources.email_listener import _get_client_recipients
from strr_email.resources.email_listener import _get_expiry_date
from strr_email.resources.email_listener import _get_rental_nickname
from strr_email.resources.email_listener import _get_service_provider
from strr_email.resources.email_listener import dict_keys_to_snake_case
from strr_email.resources.email_listener import get_email_info


class TestDictKeysToSnakeCase:
def test_converts_camel_case(self):
result = dict_keys_to_snake_case({"emailType": "HOST", "applicationNumber": "APP-001"})
assert result == {"email_type": "HOST", "application_number": "APP-001"}

def test_already_snake_case_unchanged(self):
result = dict_keys_to_snake_case({"email_type": "HOST"})
assert result == {"email_type": "HOST"}

def test_empty_dict(self):
assert dict_keys_to_snake_case({}) == {}

def test_preserves_none_values(self):
result = dict_keys_to_snake_case({"customContent": None, "registrationNumber": "H123"})
assert result["custom_content"] is None
assert result["registration_number"] == "H123"

def test_single_word_key(self):
result = dict_keys_to_snake_case({"type": "HOST"})
assert result == {"type": "HOST"}


class TestGetEmailInfo:
def test_returns_email_info_from_valid_data(self):
ce = SimpleCloudEvent(
id="id",
source="src",
subject="sub",
type="email",
data={"emailType": "HOST_RENEWAL_REMINDER", "registrationNumber": "H123"},
)
info = get_email_info(ce)
assert info is not None
assert info.email_type == "HOST_RENEWAL_REMINDER"
assert info.registration_number == "H123"

def test_returns_none_when_no_data(self):
ce = SimpleCloudEvent(id="id", source="src", subject="sub", type="email")
assert get_email_info(ce) is None

def test_returns_none_when_data_is_not_dict(self):
ce = SimpleCloudEvent(id="id", source="src", subject="sub", type="email", data="not-a-dict")
assert get_email_info(ce) is None

def test_converts_camel_case_fields(self):
ce = SimpleCloudEvent(
id="id",
source="src",
subject="sub",
type="email",
data={"emailType": "NOC", "applicationNumber": "APP-999", "customContent": "hello"},
)
info = get_email_info(ce)
assert info.email_type == "NOC"
assert info.application_number == "APP-999"
assert info.custom_content == "hello"


class TestGetAddressDetail:
def test_host_returns_requested_field(self):
app_dict = {"registration": {"unitAddress": {"streetNumber": "123"}}}
result = _get_address_detail(app_dict, Registration.RegistrationType.HOST, "streetNumber")
assert result == "123"

def test_non_host_returns_empty_string(self):
app_dict = {"registration": {"unitAddress": {"streetNumber": "123"}}}
result = _get_address_detail(
app_dict, Registration.RegistrationType.PLATFORM, "streetNumber"
)
assert result == ""

def test_missing_field_returns_empty_string(self):
app_dict = {"registration": {"unitAddress": {}}}
result = _get_address_detail(app_dict, Registration.RegistrationType.HOST, "unitNumber")
assert result == ""

def test_strata_hotel_returns_empty_string(self):
app_dict = {"registration": {"unitAddress": {"streetNumber": "456"}}}
result = _get_address_detail(
app_dict, Registration.RegistrationType.STRATA_HOTEL, "streetNumber"
)
assert result == ""


class TestGetExpiryDate:
def test_formats_valid_iso_date(self):
app_dict = {"header": {"registrationEndDate": "2025-12-15T00:00:00+00:00"}}
result = _get_expiry_date(app_dict)
assert "December" in result
assert "2025" in result

def test_returns_empty_when_no_date(self):
assert _get_expiry_date({"header": {}}) == ""

def test_returns_empty_when_no_header(self):
assert _get_expiry_date({}) == ""

def test_returns_empty_when_date_is_none(self):
assert _get_expiry_date({"header": {"registrationEndDate": None}}) == ""


class TestGetServiceProvider:
def test_platform_returns_legal_name(self):
app_dict = {"registration": {"businessDetails": {"legalName": "Acme Rentals Ltd."}}}
result = _get_service_provider(app_dict, Registration.RegistrationType.PLATFORM)
assert result == "Acme Rentals Ltd."

def test_host_returns_empty_string(self):
app_dict = {"registration": {"businessDetails": {"legalName": "Acme Rentals Ltd."}}}
result = _get_service_provider(app_dict, Registration.RegistrationType.HOST)
assert result == ""

def test_strata_hotel_returns_empty_string(self):
app_dict = {"registration": {"businessDetails": {"legalName": "Acme"}}}
result = _get_service_provider(app_dict, Registration.RegistrationType.STRATA_HOTEL)
assert result == ""


class TestGetClientRecipients:
def test_host_returns_primary_contact_email(self):
app_dict = {
"registration": {
"registrationType": Registration.RegistrationType.HOST.value,
"primaryContact": {"emailAddress": "[email protected]"},
}
}
result = _get_client_recipients(app_dict)
assert result == "[email protected]"

def test_host_with_property_manager_contact_email(self):
app_dict = {
"registration": {
"registrationType": Registration.RegistrationType.HOST.value,
"primaryContact": {"emailAddress": "[email protected]"},
"propertyManager": {"contact": {"emailAddress": "[email protected]"}},
}
}
result = _get_client_recipients(app_dict)
assert "[email protected]" in result
assert "[email protected]" in result

def test_host_with_property_manager_business_email(self):
app_dict = {
"registration": {
"registrationType": Registration.RegistrationType.HOST.value,
"primaryContact": {"emailAddress": "[email protected]"},
"propertyManager": {
"business": {"primaryContact": {"emailAddress": "[email protected]"}}
},
}
}
result = _get_client_recipients(app_dict)
assert "[email protected]" in result
assert "[email protected]" in result

def test_platform_returns_empty_string(self):
app_dict = {
"registration": {
"registrationType": Registration.RegistrationType.PLATFORM.value,
}
}
assert _get_client_recipients(app_dict) == ""

def test_strata_hotel_returns_empty_string(self):
app_dict = {
"registration": {
"registrationType": Registration.RegistrationType.STRATA_HOTEL.value,
}
}
assert _get_client_recipients(app_dict) == ""


class TestGetRentalNickname:
def test_host_with_nickname_returns_nickname(self):
app_dict = {"registration": {"unitAddress": {"nickname": "Beach House"}}}
result = _get_rental_nickname(app_dict, Registration.RegistrationType.HOST)
assert result == "Beach House"

def test_host_without_nickname_returns_none(self):
app_dict = {"registration": {"unitAddress": {}}}
result = _get_rental_nickname(app_dict, Registration.RegistrationType.HOST)
assert result is None

def test_platform_returns_none(self):
app_dict = {"registration": {"unitAddress": {"nickname": "Beach House"}}}
result = _get_rental_nickname(app_dict, Registration.RegistrationType.PLATFORM)
assert result is None

def test_strata_hotel_returns_none(self):
app_dict = {"registration": {"unitAddress": {"nickname": "Resort"}}}
result = _get_rental_nickname(app_dict, Registration.RegistrationType.STRATA_HOTEL)
assert result is None
Loading
Loading