diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py b/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py new file mode 100644 index 000000000..94c664a2a --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_dashboard.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceDashboard": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceDashboard": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceDashboard": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_field.py b/pyatlan_v9/model/assets/_overlays/salesforce_field.py new file mode 100644 index 000000000..c164110b0 --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_field.py @@ -0,0 +1,39 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + object_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceField": + validate_required_fields( + ["name", "api_name", "object_qualified_name"], + [name, api_name, object_qualified_name], + ) + fields = object_qualified_name.split("/") + if len(fields) < 5: + raise ValueError("object_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{object_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + object_qualified_name=object_qualified_name, + organization_qualified_name="/".join(fields[:-1]), + object=RelatedSalesforceObject(qualified_name=object_qualified_name), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceField": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceField": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_object.py b/pyatlan_v9/model/assets/_overlays/salesforce_object.py new file mode 100644 index 000000000..be267da4b --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_object.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceObject": + validate_required_fields( + ["name", "api_name", "organization_qualified_name"], + [name, api_name, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceObject": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceObject": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_organization.py b/pyatlan_v9/model/assets/_overlays/salesforce_organization.py new file mode 100644 index 000000000..95b7ec4dd --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_organization.py @@ -0,0 +1,32 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + connection_qualified_name: str, + source_id: str, + ) -> "SalesforceOrganization": + validate_required_fields( + ["name", "connection_qualified_name", "source_id"], + [name, connection_qualified_name, source_id], + ) + fields = connection_qualified_name.split("/") + connector_name = fields[1] if len(fields) > 1 else None + return cls( + name=name, + qualified_name=f"{connection_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=connector_name, + source_id=source_id, + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceOrganization": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceOrganization": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/_overlays/salesforce_report.py b/pyatlan_v9/model/assets/_overlays/salesforce_report.py new file mode 100644 index 000000000..b446f6593 --- /dev/null +++ b/pyatlan_v9/model/assets/_overlays/salesforce_report.py @@ -0,0 +1,40 @@ +# INTERNAL_IMPORT: from pyatlan.utils import init_guid, validate_required_fields + + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceReport": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceReport": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceReport": + return cls.updater(**kwargs) diff --git a/pyatlan_v9/model/assets/salesforce_dashboard.py b/pyatlan_v9/model/assets/salesforce_dashboard.py index ee50223a5..37cfab108 100644 --- a/pyatlan_v9/model/assets/salesforce_dashboard.py +++ b/pyatlan_v9/model/assets/salesforce_dashboard.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -323,6 +324,45 @@ def relate(self) -> "RelatedSalesforceDashboard": return RelatedSalesforceDashboard(guid=self.guid) return RelatedSalesforceDashboard(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceDashboard": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceDashboard": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceDashboard": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_field.py b/pyatlan_v9/model/assets/salesforce_field.py index a9fe84899..40214752f 100644 --- a/pyatlan_v9/model/assets/salesforce_field.py +++ b/pyatlan_v9/model/assets/salesforce_field.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -375,6 +376,44 @@ def relate(self) -> "RelatedSalesforceField": return RelatedSalesforceField(guid=self.guid) return RelatedSalesforceField(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + object_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceField": + validate_required_fields( + ["name", "api_name", "object_qualified_name"], + [name, api_name, object_qualified_name], + ) + fields = object_qualified_name.split("/") + if len(fields) < 5: + raise ValueError("object_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{object_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + object_qualified_name=object_qualified_name, + organization_qualified_name="/".join(fields[:-1]), + object=RelatedSalesforceObject(qualified_name=object_qualified_name), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceField": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceField": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_object.py b/pyatlan_v9/model/assets/salesforce_object.py index 279e88a11..826fe1386 100644 --- a/pyatlan_v9/model/assets/salesforce_object.py +++ b/pyatlan_v9/model/assets/salesforce_object.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -331,6 +332,45 @@ def relate(self) -> "RelatedSalesforceObject": return RelatedSalesforceObject(guid=self.guid) return RelatedSalesforceObject(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + api_name: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceObject": + validate_required_fields( + ["name", "api_name", "organization_qualified_name"], + [name, api_name, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{api_name}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + api_name=api_name, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceObject": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceObject": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_organization.py b/pyatlan_v9/model/assets/salesforce_organization.py index 58cb30110..544771bc5 100644 --- a/pyatlan_v9/model/assets/salesforce_organization.py +++ b/pyatlan_v9/model/assets/salesforce_organization.py @@ -24,6 +24,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -307,6 +308,37 @@ def relate(self) -> "RelatedSalesforceOrganization": return RelatedSalesforceOrganization(guid=self.guid) return RelatedSalesforceOrganization(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + connection_qualified_name: str, + source_id: str, + ) -> "SalesforceOrganization": + validate_required_fields( + ["name", "connection_qualified_name", "source_id"], + [name, connection_qualified_name, source_id], + ) + fields = connection_qualified_name.split("/") + connector_name = fields[1] if len(fields) > 1 else None + return cls( + name=name, + qualified_name=f"{connection_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=connector_name, + source_id=source_id, + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceOrganization": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceOrganization": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/pyatlan_v9/model/assets/salesforce_report.py b/pyatlan_v9/model/assets/salesforce_report.py index da9d7b494..329845154 100644 --- a/pyatlan_v9/model/assets/salesforce_report.py +++ b/pyatlan_v9/model/assets/salesforce_report.py @@ -25,6 +25,7 @@ ) from pyatlan_v9.model.serde import Serde, get_serde from pyatlan_v9.model.transform import register_asset +from pyatlan_v9.utils import init_guid, validate_required_fields from .airflow_related import RelatedAirflowTask from .anomalo_related import RelatedAnomaloCheck @@ -323,6 +324,45 @@ def relate(self) -> "RelatedSalesforceReport": return RelatedSalesforceReport(guid=self.guid) return RelatedSalesforceReport(qualified_name=self.qualified_name) + @classmethod + @init_guid + def creator( + cls, + *, + name: str, + source_id: str, + organization_qualified_name: str, + connection_qualified_name: Union[str, None] = None, + ) -> "SalesforceReport": + validate_required_fields( + ["name", "source_id", "organization_qualified_name"], + [name, source_id, organization_qualified_name], + ) + fields = organization_qualified_name.split("/") + if len(fields) < 4: + raise ValueError("organization_qualified_name is invalid") + if not connection_qualified_name: + connection_qualified_name = "/".join(fields[:3]) + return cls( + name=name, + qualified_name=f"{organization_qualified_name}/{source_id}", + connection_qualified_name=connection_qualified_name, + connector_name=fields[1], + source_id=source_id, + organization_qualified_name=organization_qualified_name, + organization=RelatedSalesforceOrganization( + qualified_name=organization_qualified_name + ), + ) + + @classmethod + def create(cls, **kwargs) -> "SalesforceReport": + return cls.creator(**kwargs) + + @classmethod + def create_for_modification(cls, **kwargs) -> "SalesforceReport": + return cls.updater(**kwargs) + # ========================================================================= # Optimized Serialization Methods (override Asset base class) # ========================================================================= diff --git a/tests_v9/integration/salesforce_asset_test.py b/tests_v9/integration/salesforce_asset_test.py new file mode 100644 index 000000000..37eec354a --- /dev/null +++ b/tests_v9/integration/salesforce_asset_test.py @@ -0,0 +1,198 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +from typing import Generator + +import pytest + +from pyatlan_v9.client.atlan import AtlanClient +from pyatlan_v9.model.assets import ( + Connection, + SalesforceDashboard, + SalesforceField, + SalesforceObject, + SalesforceOrganization, + SalesforceReport, +) +from pyatlan_v9.model.enums import AtlanConnectorType +from tests_v9.integration.client import TestId, delete_asset +from tests_v9.integration.connection_test import create_connection + +MODULE_NAME = TestId.make_unique("SALESFORCE") + +ORG_ID = f"00D{MODULE_NAME}" +OBJECT_API_NAME = f"Obj_{MODULE_NAME}__c" +FIELD_API_NAME = f"Fld_{MODULE_NAME}__c" +REPORT_ID = f"00O{MODULE_NAME}" +DASHBOARD_ID = f"01Z{MODULE_NAME}" + + +@pytest.fixture(scope="module") +def connection(client: AtlanClient) -> Generator[Connection, None, None]: + result = create_connection( + client=client, name=MODULE_NAME, connector_type=AtlanConnectorType.SALESFORCE + ) + yield result + delete_asset(client, guid=result.guid, asset_type=Connection) + + +@pytest.fixture(scope="module") +def organization( + client: AtlanClient, connection: Connection +) -> Generator[SalesforceOrganization, None, None]: + to_create = SalesforceOrganization.creator( + name=f"org_{MODULE_NAME}", + connection_qualified_name=connection.qualified_name, + source_id=ORG_ID, + ) + result = client.asset.save(to_create).assets_created( + asset_type=SalesforceOrganization + )[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceOrganization) + + +@pytest.fixture(scope="module") +def sf_object( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceObject, None, None]: + to_create = SalesforceObject.creator( + name=f"object_{MODULE_NAME}", + api_name=OBJECT_API_NAME, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceObject)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceObject) + + +@pytest.fixture(scope="module") +def sf_field( + client: AtlanClient, sf_object: SalesforceObject +) -> Generator[SalesforceField, None, None]: + to_create = SalesforceField.creator( + name=f"field_{MODULE_NAME}", + api_name=FIELD_API_NAME, + object_qualified_name=sf_object.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceField)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceField) + + +@pytest.fixture(scope="module") +def report( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceReport, None, None]: + to_create = SalesforceReport.creator( + name=f"report_{MODULE_NAME}", + source_id=REPORT_ID, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created(asset_type=SalesforceReport)[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceReport) + + +@pytest.fixture(scope="module") +def dashboard( + client: AtlanClient, organization: SalesforceOrganization +) -> Generator[SalesforceDashboard, None, None]: + to_create = SalesforceDashboard.creator( + name=f"dashboard_{MODULE_NAME}", + source_id=DASHBOARD_ID, + organization_qualified_name=organization.qualified_name, + ) + result = client.asset.save(to_create).assets_created( + asset_type=SalesforceDashboard + )[0] + yield result + delete_asset(client, guid=result.guid, asset_type=SalesforceDashboard) + + +def _retrieve(client, asset, asset_type, connection, expected_qn): + retrieved = client.asset.get_by_guid( + asset.guid, asset_type=asset_type, ignore_relationships=False + ) + assert retrieved.qualified_name == expected_qn + assert retrieved.name == asset.name + assert retrieved.connector_name == AtlanConnectorType.SALESFORCE + assert retrieved.connection_qualified_name == connection.qualified_name + return retrieved + + +def _org_qn(connection: Connection) -> str: + return f"{connection.qualified_name}/{ORG_ID}" + + +def test_organization( + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, +): + retrieved = _retrieve( + client, organization, SalesforceOrganization, connection, _org_qn(connection) + ) + assert retrieved.source_id == ORG_ID + + +def test_object( + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, + sf_object: SalesforceObject, +): + retrieved = _retrieve( + client, + sf_object, + SalesforceObject, + connection, + f"{_org_qn(connection)}/{OBJECT_API_NAME}", + ) + assert retrieved.api_name == OBJECT_API_NAME + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.organization.guid == organization.guid + + +def test_field( + client: AtlanClient, + connection: Connection, + sf_object: SalesforceObject, + sf_field: SalesforceField, +): + retrieved = _retrieve( + client, + sf_field, + SalesforceField, + connection, + f"{_org_qn(connection)}/{OBJECT_API_NAME}/{FIELD_API_NAME}", + ) + assert retrieved.api_name == FIELD_API_NAME + assert retrieved.object_qualified_name == sf_object.qualified_name + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.object.guid == sf_object.guid + + +@pytest.mark.parametrize( + "fixture_name, asset_type, source_id", + [ + ("report", SalesforceReport, REPORT_ID), + ("dashboard", SalesforceDashboard, DASHBOARD_ID), + ], +) +def test_report_and_dashboard( + request, + client: AtlanClient, + connection: Connection, + organization: SalesforceOrganization, + fixture_name, + asset_type, + source_id, +): + asset = request.getfixturevalue(fixture_name) + retrieved = _retrieve( + client, asset, asset_type, connection, f"{_org_qn(connection)}/{source_id}" + ) + assert retrieved.source_id == source_id + assert retrieved.organization_qualified_name == _org_qn(connection) + assert retrieved.organization.guid == organization.guid diff --git a/tests_v9/unit/model/salesforce_test.py b/tests_v9/unit/model/salesforce_test.py new file mode 100644 index 000000000..df3a19f1f --- /dev/null +++ b/tests_v9/unit/model/salesforce_test.py @@ -0,0 +1,204 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for the Salesforce creators in pyatlan_v9. + +The expected qualifiedNames are the exact strings atlan-salesforce-app emits today +(FND-3838 census), so a creator change that would re-key existing Salesforce assets +fails here. +""" + +import pytest + +from pyatlan_v9.model import ( + SalesforceDashboard, + SalesforceField, + SalesforceObject, + SalesforceOrganization, + SalesforceReport, +) + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_ID = "00D5g000004XyZaEAK" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +OBJECT_API_NAME = "Account" +OBJECT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" +FIELD_API_NAME = "Annual_Revenue__c" +FIELD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" +REPORT_ID = "00O5g000004AbCdEAF" +REPORT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" +DASHBOARD_ID = "01Z5g000000AbCdEAF" +DASHBOARD_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" +NAME = "Quarterly Revenue" + + +def _assert_common(sut, expected_qn): + assert sut.qualified_name == expected_qn + assert sut.name == NAME + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + # SalesforceOrganization has no containment parent, so it has no pattern. + pattern = getattr(sut, "_QUALIFIED_NAME_PATTERN", None) + if pattern is not None: + assert pattern.match(sut.qualified_name) + sut.validate() + + +def test_organization_creator_matches_app_qualified_name(): + sut = SalesforceOrganization.creator( + name=NAME, connection_qualified_name=CONNECTION_QN, source_id=ORG_ID + ) + + _assert_common(sut, ORG_QN) + assert sut.source_id == ORG_ID + sut.validate(for_creation=True) + + +def test_object_creator_matches_app_qualified_name(): + sut = SalesforceObject.creator( + name=NAME, api_name=OBJECT_API_NAME, organization_qualified_name=ORG_QN + ) + + _assert_common(sut, OBJECT_QN) + assert sut.api_name == OBJECT_API_NAME + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + sut.validate(for_creation=True) + + +def test_field_creator_matches_app_qualified_name(): + sut = SalesforceField.creator( + name=NAME, api_name=FIELD_API_NAME, object_qualified_name=OBJECT_QN + ) + + _assert_common(sut, FIELD_QN) + assert sut.api_name == FIELD_API_NAME + assert sut.object_qualified_name == OBJECT_QN + assert sut.organization_qualified_name == ORG_QN + assert sut.object.qualified_name == OBJECT_QN + sut.validate(for_creation=True) + + +@pytest.mark.parametrize( + "asset_type, source_id, expected_qn", + [ + (SalesforceReport, REPORT_ID, REPORT_QN), + (SalesforceDashboard, DASHBOARD_ID, DASHBOARD_QN), + ], +) +def test_report_and_dashboard_creator_matches_app_qualified_name( + asset_type, source_id, expected_qn +): + sut = asset_type.creator( + name=NAME, source_id=source_id, organization_qualified_name=ORG_QN + ) + + _assert_common(sut, expected_qn) + assert sut.source_id == source_id + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + sut.validate(for_creation=True) + + +@pytest.mark.parametrize( + "asset_type, kwargs", + [ + ( + SalesforceObject, + {"api_name": OBJECT_API_NAME, "organization_qualified_name": ORG_QN}, + ), + ( + SalesforceField, + {"api_name": FIELD_API_NAME, "object_qualified_name": OBJECT_QN}, + ), + ( + SalesforceReport, + {"source_id": REPORT_ID, "organization_qualified_name": ORG_QN}, + ), + ( + SalesforceDashboard, + {"source_id": DASHBOARD_ID, "organization_qualified_name": ORG_QN}, + ), + ], +) +def test_explicit_connection_qualified_name_is_kept(asset_type, kwargs): + sut = asset_type.creator( + name=NAME, connection_qualified_name=CONNECTION_QN, **kwargs + ) + + assert sut.connection_qualified_name == CONNECTION_QN + + +VALID_KWARGS = { + SalesforceOrganization: { + "name": NAME, + "connection_qualified_name": CONNECTION_QN, + "source_id": ORG_ID, + }, + SalesforceObject: { + "name": NAME, + "api_name": OBJECT_API_NAME, + "organization_qualified_name": ORG_QN, + }, + SalesforceField: { + "name": NAME, + "api_name": FIELD_API_NAME, + "object_qualified_name": OBJECT_QN, + }, + SalesforceReport: { + "name": NAME, + "source_id": REPORT_ID, + "organization_qualified_name": ORG_QN, + }, + SalesforceDashboard: { + "name": NAME, + "source_id": DASHBOARD_ID, + "organization_qualified_name": ORG_QN, + }, +} + +REQUIRED = [ + (asset_type, key) for asset_type, kwargs in VALID_KWARGS.items() for key in kwargs +] + + +@pytest.mark.parametrize("asset_type, key", REQUIRED) +def test_creator_rejects_missing_input(asset_type, key): + kwargs = {**VALID_KWARGS[asset_type], key: None} + + with pytest.raises(ValueError, match=f"{key} is required"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize("asset_type, key", REQUIRED) +def test_creator_rejects_blank_input(asset_type, key): + kwargs = {**VALID_KWARGS[asset_type], key: " "} + + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize( + "asset_type, key, value", + [ + (SalesforceObject, "organization_qualified_name", CONNECTION_QN), + (SalesforceReport, "organization_qualified_name", CONNECTION_QN), + (SalesforceDashboard, "organization_qualified_name", CONNECTION_QN), + (SalesforceField, "object_qualified_name", ORG_QN), + ], +) +def test_creator_rejects_too_short_parent_qualified_name(asset_type, key, value): + kwargs = {**VALID_KWARGS[asset_type], key: value} + + with pytest.raises(ValueError, match=f"{key} is invalid"): + asset_type.creator(**kwargs) + + +@pytest.mark.parametrize("asset_type", list(VALID_KWARGS)) +def test_create_alias(asset_type): + kwargs = VALID_KWARGS[asset_type] + + assert ( + asset_type.create(**kwargs).qualified_name + == asset_type.creator(**kwargs).qualified_name + )