import importlib.util import io import json import sys import types import unittest from datetime import datetime, timedelta, timezone from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parents[2] VENDORED_PYMYSQL = ( ROOT / "aws-lambda-apac-copy" / "emea-source" / "unpacked" / "EMEA-Tenants-Scoring-Task" ) sys.path.insert(0, str(VENDORED_PYMYSQL)) fake_boto3 = types.ModuleType("boto3") fake_boto3.client = lambda service_name: None sys.modules.setdefault("boto3", fake_boto3) try: import pymysql # noqa: F401 except ModuleNotFoundError: fake_pymysql = types.ModuleType("pymysql") fake_pymysql.cursors = types.SimpleNamespace(DictCursor=object) fake_pymysql.connect = lambda **kwargs: None sys.modules["pymysql"] = fake_pymysql DISPATCHER_PATH = ( ROOT / "apac_lambda_tasks" / "APAC-Tenants-Monitor-Fanout-Dispatcher-Task" / "dispatcher_function.py" ) sys.path.insert(0, str(DISPATCHER_PATH.parent)) spec = importlib.util.spec_from_file_location("apac_dispatcher_function", DISPATCHER_PATH) dispatcher_function = importlib.util.module_from_spec(spec) spec.loader.exec_module(dispatcher_function) import hubspot_sync # noqa: E402 class FakeConnection: def __init__(self): self.rollbacks = 0 self.closed = False def rollback(self): self.rollbacks += 1 def close(self): self.closed = True class DispatcherFunctionTest(unittest.TestCase): def test_new_signup_query_uses_previous_cst_natural_day(self): sql = dispatcher_function.DEFAULT_NEW_SIGNUP_SELECT_SQL self.assertIn("DATE_ADD(UTC_TIMESTAMP(), INTERVAL 8 HOUR)", sql) self.assertIn("INTERVAL 1 DAY", sql) self.assertNotIn("UTC_DATE()", sql) def test_effective_owner_sync_uses_exact_selected_tenant_batch(self): client = mock.Mock() client.invoke.return_value = { "StatusCode": 200, "Payload": io.BytesIO( json.dumps( { "sync_id": "run-1", "tenant_count": 2, "rule_version": "rule-1", } ).encode("utf-8") ), } with ( mock.patch.object( dispatcher_function, "lambda_client", return_value=client ), mock.patch.dict( dispatcher_function.os.environ, {"EFFECTIVE_OWNER_SYNC_FUNCTION_NAME": "owner-sync"}, ), ): result = dispatcher_function.sync_effective_owners( [{"id": "1"}, {"id": "2"}, {"id": "1"}], "run-1" ) payload = json.loads(client.invoke.call_args.kwargs["Payload"]) self.assertEqual(["1", "2"], payload["tenant_ids"]) self.assertFalse(payload["dry_run"]) self.assertEqual("run-1", result["effective_owner_sync_id"]) self.assertEqual(2, result["effective_owner_tenant_count"]) def test_effective_owner_function_error_blocks_dispatcher(self): client = mock.Mock() client.invoke.return_value = { "StatusCode": 200, "FunctionError": "Unhandled", "Payload": io.BytesIO( b'{"errorType":"RuntimeError","errorMessage":"redacted"}' ), } with ( mock.patch.object( dispatcher_function, "lambda_client", return_value=client ), mock.patch.dict( dispatcher_function.os.environ, {"EFFECTIVE_OWNER_SYNC_FUNCTION_NAME": "owner-sync"}, ), ): with self.assertRaisesRegex( RuntimeError, "Effective Owner sync failed: RuntimeError" ): dispatcher_function.sync_effective_owners( [{"id": "1"}], "run-1" ) def test_sync_failure_uses_last_successful_snapshot(self): now = datetime(2026, 7, 14, tzinfo=timezone.utc) connection = FakeConnection() prior_state = { "last_successful_sync_id": "prior-sync", "last_successful_sync_at": now - timedelta(days=1), "last_successful_list_total": 271, "last_attempt_status": "failed", } with ( mock.patch.object( hubspot_sync, "target_db_connect", return_value=connection ), mock.patch.object( hubspot_sync, "build_hubspot_snapshot", side_effect=RuntimeError("HubSpot unavailable"), ), mock.patch.object( hubspot_sync, "record_hubspot_sync_failure" ) as record_failure, mock.patch.object( hubspot_sync, "read_hubspot_sync_state", return_value=prior_state, ), ): result = hubspot_sync.refresh_hubspot_snapshot("run-1", now) record_failure.assert_called_once() self.assertEqual("prior-sync", result["hubspot_sync_id"]) self.assertEqual("stale", result["hubspot_snapshot_status"]) self.assertEqual(271, result["hubspot_list_total"]) self.assertTrue(connection.closed) def test_company_normalization_keeps_match_fields(self): now = datetime(2026, 7, 14, tzinfo=timezone.utc) company = { "id": "123", "properties": { "portal_id__c": "42", "account_id__c": "IBG-B-1", "domain": "https://www.example.com/path", "website": "www.example.com", "hs_additional_domains": "example.com.sg; sub.other.co.in", }, } row = hubspot_sync.normalized_hubspot_company( company, None, "sync-1", now ) self.assertEqual("42", row["portal_id"]) self.assertEqual("IBG-B-1", row["account_id"]) self.assertEqual("example.com", row["domain_root"]) self.assertIn("example.com.sg", row["additional_domain_roots"]) if __name__ == "__main__": unittest.main()