611 lines
20 KiB
Python
611 lines
20 KiB
Python
# #region TestMappingService [C:2] [TYPE Module]
|
|
#
|
|
# @PURPOSE: Unit tests for the IdMappingService matching UUIDs to integer IDs.
|
|
# @LAYER Domain
|
|
# @RELATION BINDS_TO ->[IdMappingService]
|
|
#
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
import pytest
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from sqlalchemy import create_engine, event
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
# Add backend directory to sys.path so 'src' can be resolved
|
|
backend_dir = str(Path(__file__).parent.parent.parent.resolve())
|
|
if backend_dir not in sys.path:
|
|
sys.path.insert(0, backend_dir)
|
|
|
|
from src.core.mapping_service import IdMappingService
|
|
from src.models.mapping import Base, Environment, ResourceMapping, ResourceType
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
# In-memory SQLite for testing
|
|
engine = create_engine("sqlite:///:memory:")
|
|
event.listen(engine, "connect", lambda c, _: c.execute("PRAGMA foreign_keys=ON"))
|
|
Base.metadata.create_all(engine)
|
|
Session = sessionmaker(bind=engine)
|
|
session = Session()
|
|
# Create FK parent Environment records for all environment_ids used in tests
|
|
for env_id in ['test-env', 'prod-env-1', 'env1']:
|
|
if not session.query(Environment).filter(Environment.id == env_id).first():
|
|
session.add(Environment(
|
|
id=env_id, name=env_id,
|
|
url=f'http://{env_id}.example.com',
|
|
credentials_id=env_id
|
|
))
|
|
session.commit()
|
|
yield session
|
|
session.close()
|
|
|
|
|
|
class MockSupersetClient:
|
|
def __init__(self, resources):
|
|
self.resources = resources
|
|
|
|
async def get_all_resources(self, endpoint, since_dttm=None):
|
|
return self.resources.get(endpoint, [])
|
|
|
|
|
|
# #region test_sync_environment_upserts_correctly [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_upserts_correctly(db_session):
|
|
service = IdMappingService(db_session)
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{
|
|
"id": 42,
|
|
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
|
"slice_name": "Test Chart",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
await service.sync_environment("test-env", mock_client)
|
|
|
|
mapping = db_session.query(ResourceMapping).first()
|
|
assert mapping is not None
|
|
assert mapping.environment_id == "test-env"
|
|
assert mapping.resource_type == ResourceType.CHART
|
|
assert mapping.uuid == "123e4567-e89b-12d3-a456-426614174000"
|
|
assert mapping.remote_integer_id == "42"
|
|
assert mapping.resource_name == "Test Chart"
|
|
|
|
|
|
# #endregion test_sync_environment_upserts_correctly
|
|
|
|
|
|
# #region test_get_remote_id_returns_integer [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_id_returns_integer(db_session):
|
|
service = IdMappingService(db_session)
|
|
mapping = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DATASET,
|
|
uuid="uuid-1",
|
|
remote_integer_id="99",
|
|
resource_name="Test DS",
|
|
last_synced_at=datetime.now(UTC),
|
|
)
|
|
db_session.add(mapping)
|
|
db_session.commit()
|
|
|
|
result = service.get_remote_id("test-env", ResourceType.DATASET, "uuid-1")
|
|
assert result == 99
|
|
|
|
|
|
# #endregion test_get_remote_id_returns_integer
|
|
|
|
|
|
# #region test_get_remote_ids_batch_returns_dict [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_ids_batch_returns_dict(db_session):
|
|
service = IdMappingService(db_session)
|
|
m1 = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DASHBOARD,
|
|
uuid="uuid-1",
|
|
remote_integer_id="11",
|
|
)
|
|
m2 = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DASHBOARD,
|
|
uuid="uuid-2",
|
|
remote_integer_id="22",
|
|
)
|
|
db_session.add_all([m1, m2])
|
|
db_session.commit()
|
|
|
|
result = service.get_remote_ids_batch(
|
|
"test-env", ResourceType.DASHBOARD, ["uuid-1", "uuid-2", "uuid-missing"]
|
|
)
|
|
|
|
assert len(result) == 2
|
|
assert result["uuid-1"] == 11
|
|
assert result["uuid-2"] == 22
|
|
assert "uuid-missing" not in result
|
|
|
|
|
|
# #endregion test_get_remote_ids_batch_returns_dict
|
|
|
|
|
|
# #region test_sync_environment_updates_existing_mapping [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_updates_existing_mapping(db_session):
|
|
"""Verify that sync_environment updates an existing mapping (upsert UPDATE path)."""
|
|
from src.models.mapping import ResourceMapping
|
|
|
|
# Pre-populate a mapping
|
|
existing = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.CHART,
|
|
uuid="123e4567-e89b-12d3-a456-426614174000",
|
|
remote_integer_id="10",
|
|
resource_name="Old Name",
|
|
)
|
|
db_session.add(existing)
|
|
db_session.commit()
|
|
|
|
service = IdMappingService(db_session)
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{
|
|
"id": 42,
|
|
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
|
"slice_name": "Updated Name",
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
await service.sync_environment("test-env", mock_client)
|
|
|
|
mapping = (
|
|
db_session.query(ResourceMapping)
|
|
.filter_by(uuid="123e4567-e89b-12d3-a456-426614174000")
|
|
.first()
|
|
)
|
|
assert mapping.remote_integer_id == "42"
|
|
assert mapping.resource_name == "Updated Name"
|
|
# Should still be only one record (updated, not duplicated)
|
|
count = db_session.query(ResourceMapping).count()
|
|
assert count == 1
|
|
|
|
|
|
# #endregion test_sync_environment_updates_existing_mapping
|
|
|
|
|
|
# #region test_sync_environment_skips_resources_without_uuid [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_skips_resources_without_uuid(db_session):
|
|
"""Resources missing uuid or having id=None should be silently skipped."""
|
|
service = IdMappingService(db_session)
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{"id": 42, "slice_name": "No UUID"}, # Missing 'uuid' -> skipped
|
|
{
|
|
"id": None,
|
|
"uuid": "valid-uuid",
|
|
"slice_name": "ID is None",
|
|
}, # id=None -> skipped
|
|
{
|
|
"id": None,
|
|
"uuid": None,
|
|
"slice_name": "Both None",
|
|
}, # both None -> skipped
|
|
]
|
|
}
|
|
)
|
|
|
|
await service.sync_environment("test-env", mock_client)
|
|
|
|
count = db_session.query(ResourceMapping).count()
|
|
assert count == 0
|
|
|
|
|
|
# #endregion test_sync_environment_skips_resources_without_uuid
|
|
|
|
|
|
# #region test_sync_environment_handles_api_error_gracefully [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_handles_api_error_gracefully(db_session):
|
|
"""If one resource type fails, others should still sync."""
|
|
|
|
class FailingClient:
|
|
async def get_all_resources(self, endpoint, since_dttm=None):
|
|
if endpoint == "chart":
|
|
raise ConnectionError("API timeout")
|
|
if endpoint == "dataset":
|
|
return [{"id": 99, "uuid": "ds-uuid-1", "table_name": "users"}]
|
|
return []
|
|
|
|
service = IdMappingService(db_session)
|
|
await service.sync_environment("test-env", FailingClient())
|
|
|
|
count = db_session.query(ResourceMapping).count()
|
|
assert count == 1 # Only dataset was synced; chart error was swallowed
|
|
mapping = db_session.query(ResourceMapping).first()
|
|
assert mapping.resource_type == ResourceType.DATASET
|
|
|
|
|
|
# #endregion test_sync_environment_handles_api_error_gracefully
|
|
|
|
|
|
# #region test_get_remote_id_returns_none_for_missing [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_id_returns_none_for_missing(db_session):
|
|
"""get_remote_id should return None when no mapping exists."""
|
|
service = IdMappingService(db_session)
|
|
result = service.get_remote_id("test-env", ResourceType.CHART, "nonexistent-uuid")
|
|
assert result is None
|
|
|
|
|
|
# #endregion test_get_remote_id_returns_none_for_missing
|
|
|
|
|
|
# #region test_get_remote_ids_batch_returns_empty_for_empty_input [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_ids_batch_returns_empty_for_empty_input(db_session):
|
|
"""get_remote_ids_batch should return {} for an empty list of UUIDs."""
|
|
service = IdMappingService(db_session)
|
|
result = service.get_remote_ids_batch("test-env", ResourceType.CHART, [])
|
|
assert result == {}
|
|
|
|
|
|
# #endregion test_get_remote_ids_batch_returns_empty_for_empty_input
|
|
|
|
|
|
# #region test_mapping_service_alignment_with_test_data [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_mapping_service_alignment_with_test_data(db_session):
|
|
"""**@TEST_DATA**: Verifies that the service aligns with the resource_mapping_record contract."""
|
|
# Contract: {'environment_id': 'prod-env-1', 'resource_type': 'chart', 'uuid': '123e4567-e89b-12d3-a456-426614174000', 'remote_integer_id': '42'}
|
|
contract_data = {
|
|
"environment_id": "prod-env-1",
|
|
"resource_type": ResourceType.CHART,
|
|
"uuid": "123e4567-e89b-12d3-a456-426614174000",
|
|
"remote_integer_id": "42",
|
|
}
|
|
|
|
mapping = ResourceMapping(**contract_data)
|
|
db_session.add(mapping)
|
|
db_session.commit()
|
|
|
|
service = IdMappingService(db_session)
|
|
result = service.get_remote_id(
|
|
contract_data["environment_id"],
|
|
contract_data["resource_type"],
|
|
contract_data["uuid"],
|
|
)
|
|
|
|
assert result == 42
|
|
|
|
|
|
# #endregion test_mapping_service_alignment_with_test_data
|
|
|
|
|
|
# #region test_sync_environment_requires_existing_env [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_requires_existing_env(db_session):
|
|
"""**@PRE**: Verify behavior when environment_id is invalid/missing in DB.
|
|
Note: The current implementation doesn't strictly check for environment existencia in the DB
|
|
before polling, but it should handle it gracefully or follow the contract.
|
|
"""
|
|
service = IdMappingService(db_session)
|
|
mock_client = MockSupersetClient({"chart": []})
|
|
|
|
# Even if environment doesn't exist in a hypothetical 'environments' table,
|
|
# the service should still complete or fail according to defined error handling.
|
|
# In GRACE-Poly, @PRE is a hard requirement. If we don't have an Env model check,
|
|
# we simulate the intent.
|
|
|
|
await service.sync_environment("non-existent-env", mock_client)
|
|
# If no error raised, at least verify no mappings were created for other envs
|
|
assert db_session.query(ResourceMapping).count() == 0
|
|
|
|
|
|
# #endregion test_sync_environment_requires_existing_env
|
|
|
|
|
|
# #region test_sync_environment_deletes_stale_mappings [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_deletes_stale_mappings(db_session):
|
|
"""Verify that mappings for resources deleted from the remote environment
|
|
are removed from the local DB on the next sync cycle."""
|
|
service = IdMappingService(db_session)
|
|
|
|
# First sync: 2 charts exist
|
|
client_v1 = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
|
{"id": 2, "uuid": "bbb", "slice_name": "Chart B"},
|
|
]
|
|
}
|
|
)
|
|
await service.sync_environment("env1", client_v1)
|
|
assert (
|
|
db_session.query(ResourceMapping).filter_by(environment_id="env1").count() == 2
|
|
)
|
|
|
|
# Second sync: user deleted Chart B from superset
|
|
client_v2 = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
|
]
|
|
}
|
|
)
|
|
await service.sync_environment("env1", client_v2)
|
|
|
|
remaining = db_session.query(ResourceMapping).filter_by(environment_id="env1").all()
|
|
assert len(remaining) == 1
|
|
assert remaining[0].uuid == "aaa"
|
|
|
|
|
|
# #endregion test_sync_environment_deletes_stale_mappings
|
|
|
|
|
|
# #region test_start_scheduler_basic [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@patch("src.core.mapping_service.seed_trace_id")
|
|
@patch("src.core.mapping_service.BackgroundScheduler")
|
|
def test_start_scheduler_basic(mock_scheduler_cls, mock_seed, db_session):
|
|
"""start_scheduler adds job and starts scheduler."""
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler_cls.return_value = mock_scheduler
|
|
mock_scheduler.running = False
|
|
|
|
service = IdMappingService(db_session)
|
|
mock_factory = MagicMock(return_value=MagicMock())
|
|
service.start_scheduler("0 */5 * * *", ["env1", "env2"], mock_factory)
|
|
|
|
mock_scheduler.add_job.assert_called_once()
|
|
mock_scheduler.start.assert_called_once()
|
|
|
|
|
|
# #endregion test_start_scheduler_basic
|
|
|
|
|
|
# #region test_start_scheduler_sync_all_called [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_start_scheduler_sync_all_called(db_session):
|
|
"""sync_all inner function executed via scheduler.add_job (lines 75-80)."""
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler.running = False
|
|
|
|
captured_fn = [None]
|
|
|
|
def _capture_job(fn, *args, **kwargs):
|
|
captured_fn[0] = fn
|
|
return MagicMock(id="sync-job")
|
|
|
|
mock_scheduler.add_job.side_effect = _capture_job
|
|
|
|
mock_factory = MagicMock(return_value=MagicMock())
|
|
|
|
with patch("src.core.mapping_service.seed_trace_id") as mock_seed, \
|
|
patch("src.core.mapping_service.BackgroundScheduler", return_value=mock_scheduler):
|
|
|
|
service = IdMappingService(db_session)
|
|
with patch.object(service, 'sync_environment', return_value=None) as mock_sync:
|
|
service.start_scheduler("0 */5 * * *", ["env1", "env2"], mock_factory)
|
|
# Call the captured sync_all
|
|
captured_fn[0]()
|
|
|
|
mock_seed.assert_called_once()
|
|
assert mock_factory.call_count == 2
|
|
mock_factory.assert_any_call("env1")
|
|
mock_factory.assert_any_call("env2")
|
|
assert mock_sync.call_count == 2
|
|
# #endregion test_start_scheduler_sync_all_called
|
|
|
|
|
|
# #region test_start_scheduler_update_existing [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@patch("src.core.mapping_service.seed_trace_id")
|
|
@patch("src.core.mapping_service.BackgroundScheduler")
|
|
def test_start_scheduler_update_existing(mock_scheduler_cls, mock_seed, db_session):
|
|
"""start_scheduler with existing job → updates, doesn't re-start scheduler."""
|
|
mock_scheduler = MagicMock()
|
|
mock_scheduler_cls.return_value = mock_scheduler
|
|
mock_scheduler.running = False # Not running initially
|
|
|
|
service = IdMappingService(db_session)
|
|
mock_factory = MagicMock(return_value=MagicMock())
|
|
service.start_scheduler("0 */5 * * *", ["env1"], mock_factory)
|
|
|
|
# First call should start the scheduler (was not running)
|
|
mock_scheduler.start.assert_called_once()
|
|
|
|
# Second call with existing job — scheduler already running
|
|
mock_scheduler.running = True
|
|
service.start_scheduler("0 */10 * * *", ["env1"], mock_factory)
|
|
|
|
mock_scheduler.remove_job.assert_called_once() # called because _sync_job exists
|
|
assert mock_scheduler.add_job.call_count == 2
|
|
|
|
|
|
# #endregion test_start_scheduler_update_existing
|
|
|
|
|
|
# #region test_get_remote_id_value_error [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_id_value_error(db_session):
|
|
"""get_remote_id with non-integer remote_integer_id → None."""
|
|
service = IdMappingService(db_session)
|
|
mapping = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DATASET,
|
|
uuid="uuid-bad",
|
|
remote_integer_id="not-a-number",
|
|
)
|
|
db_session.add(mapping)
|
|
db_session.commit()
|
|
|
|
result = service.get_remote_id("test-env", ResourceType.DATASET, "uuid-bad")
|
|
assert result is None
|
|
|
|
|
|
# #endregion test_get_remote_id_value_error
|
|
|
|
|
|
# #region test_get_remote_ids_batch_value_error [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
def test_get_remote_ids_batch_value_error(db_session):
|
|
"""get_remote_ids_batch with non-integer remote_integer_id → skipped."""
|
|
service = IdMappingService(db_session)
|
|
m1 = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DASHBOARD,
|
|
uuid="uuid-ok",
|
|
remote_integer_id="42",
|
|
)
|
|
m2 = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.DASHBOARD,
|
|
uuid="uuid-bad",
|
|
remote_integer_id="not-a-number",
|
|
)
|
|
db_session.add_all([m1, m2])
|
|
db_session.commit()
|
|
|
|
result = service.get_remote_ids_batch(
|
|
"test-env", ResourceType.DASHBOARD, ["uuid-ok", "uuid-bad"]
|
|
)
|
|
assert result["uuid-ok"] == 42
|
|
assert "uuid-bad" not in result
|
|
|
|
|
|
# #endregion test_get_remote_ids_batch_value_error
|
|
|
|
|
|
# #region test_sync_environment_incremental [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_incremental(db_session):
|
|
"""incremental=True uses max_date filter for since_dttm."""
|
|
service = IdMappingService(db_session)
|
|
|
|
# Add an existing mapping with a last_synced_at value
|
|
from datetime import timedelta
|
|
old_mapping = ResourceMapping(
|
|
environment_id="test-env",
|
|
resource_type=ResourceType.CHART,
|
|
uuid="existing-uuid",
|
|
remote_integer_id="1",
|
|
resource_name="Old Chart",
|
|
last_synced_at=datetime.now(UTC) - timedelta(days=1),
|
|
)
|
|
db_session.add(old_mapping)
|
|
db_session.commit()
|
|
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{
|
|
"id": 42,
|
|
"uuid": "existing-uuid",
|
|
"slice_name": "Updated Chart",
|
|
},
|
|
{
|
|
"id": 99,
|
|
"uuid": "new-uuid",
|
|
"slice_name": "New Chart",
|
|
},
|
|
]
|
|
}
|
|
)
|
|
|
|
await service.sync_environment("test-env", mock_client, incremental=True)
|
|
|
|
# Both existing and new should be synced
|
|
mappings = db_session.query(ResourceMapping).filter_by(environment_id="test-env").all()
|
|
assert len(mappings) == 2
|
|
|
|
# Existing mapping should be updated
|
|
updated = db_session.query(ResourceMapping).filter_by(uuid="existing-uuid").first()
|
|
assert updated.remote_integer_id == "42"
|
|
assert updated.resource_name == "Updated Chart"
|
|
|
|
# New mapping created
|
|
new_mapping = db_session.query(ResourceMapping).filter_by(uuid="new-uuid").first()
|
|
assert new_mapping is not None
|
|
assert new_mapping.remote_integer_id == "99"
|
|
|
|
|
|
# #endregion test_sync_environment_incremental
|
|
|
|
|
|
# #region test_sync_environment_no_new_data_incremental [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_no_new_data_incremental(db_session):
|
|
"""incremental=True with no existing last_synced_at → since_dttm is None."""
|
|
service = IdMappingService(db_session)
|
|
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{"id": 1, "uuid": "aaa", "slice_name": "Chart A"},
|
|
],
|
|
"dataset": [],
|
|
"dashboard": [],
|
|
}
|
|
)
|
|
await service.sync_environment("test-env", mock_client, incremental=True)
|
|
|
|
count = db_session.query(ResourceMapping).count()
|
|
assert count == 1
|
|
mapping = db_session.query(ResourceMapping).first()
|
|
assert mapping.uuid == "aaa"
|
|
|
|
|
|
# #endregion test_sync_environment_no_new_data_incremental
|
|
|
|
|
|
# #region test_sync_environment_critical_failure [C:2] [TYPE Function]
|
|
# @RELATION BINDS_TO ->[TestMappingService]
|
|
@pytest.mark.asyncio
|
|
async def test_sync_environment_critical_failure(db_session):
|
|
"""Exception outside resource loop → rollback and re-raise."""
|
|
service = IdMappingService(db_session)
|
|
|
|
service = IdMappingService(db_session)
|
|
# Simulate a commit failure — raise before actual commit
|
|
def broken_commit():
|
|
raise Exception("commit failure")
|
|
db_session.commit = broken_commit
|
|
|
|
mock_client = MockSupersetClient(
|
|
{
|
|
"chart": [
|
|
{"id": 1, "uuid": "uuid-1", "slice_name": "Chart A"},
|
|
]
|
|
}
|
|
)
|
|
|
|
with pytest.raises(Exception, match="commit failure"):
|
|
await service.sync_environment("test-env", mock_client)
|
|
|
|
# DB should have rolled back (rollback clears uncommitted changes)
|
|
assert db_session.query(ResourceMapping).count() == 0
|
|
|
|
|
|
# #endregion test_sync_environment_critical_failure
|
|
# #endregion TestMappingService
|