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..4358a1c70 100644 --- a/pyatlan_v9/model/assets/salesforce_organization.py +++ b/pyatlan_v9/model/assets/salesforce_organization.py @@ -14,6 +14,7 @@ from __future__ import annotations +import re from typing import Any, ClassVar, List, Union from msgspec import UNSET, UnsetType @@ -24,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 @@ -249,6 +251,8 @@ def __post_init__(self) -> None: # SDK Methods # ========================================================================= + _QUALIFIED_NAME_PATTERN: ClassVar[re.Pattern] = re.compile(r"^.+/[^/]+$") + def validate(self, for_creation: bool = False) -> None: """ Dry-run validation of this SalesforceOrganization instance. @@ -273,6 +277,11 @@ def validate(self, for_creation: bool = False) -> None: errors.append("name is required") if self.qualified_name is UNSET or self.qualified_name is None: errors.append("qualified_name is required") + elif not self._QUALIFIED_NAME_PATTERN.match(self.qualified_name): + errors.append( + f"qualified_name '{self.qualified_name}' does not match expected " + f"pattern: {self._QUALIFIED_NAME_PATTERN.pattern}" + ) if errors: raise ValueError(f"SalesforceOrganization validation failed: {errors}") @@ -307,6 +316,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_dashboard_test.py b/tests_v9/unit/model/salesforce_dashboard_test.py new file mode 100644 index 000000000..0b0ee1f54 --- /dev/null +++ b/tests_v9/unit/model/salesforce_dashboard_test.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceDashboard.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_dashboard, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceDashboard + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Quarterly Revenue" +VALID = { + "name": NAME, + "source_id": "01Z5g000000AbCdEAF", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceDashboard.creator(**VALID) + + assert ( + sut.qualified_name + == "default/salesforce/1712345678/00D5g000004XyZaEAK/01Z5g000000AbCdEAF" + ) + assert sut.name == NAME + assert sut.source_id == "01Z5g000000AbCdEAF" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceDashboard.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceDashboard.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceDashboard.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceDashboard.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceDashboard.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceDashboard.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceDashboard.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceDashboard.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceDashboard.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceDashboard) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_field_test.py b/tests_v9/unit/model/salesforce_field_test.py new file mode 100644 index 000000000..cb6a30d94 --- /dev/null +++ b/tests_v9/unit/model/salesforce_field_test.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceField.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_field, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceField + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +OBJECT_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" +NAME = "Annual Revenue" +VALID = { + "name": NAME, + "api_name": "Annual_Revenue__c", + "object_qualified_name": OBJECT_QN, +} +EXPECTED_QN = ( + "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" +) + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceField.creator(**VALID) + + assert sut.qualified_name == ( + "default/salesforce/1712345678/00D5g000004XyZaEAK/Account/Annual_Revenue__c" + ) + assert sut.name == NAME + assert sut.api_name == "Annual_Revenue__c" + assert sut.object_qualified_name == OBJECT_QN + assert sut.organization_qualified_name == ORG_QN + assert sut.object.qualified_name == OBJECT_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [ORG_QN, "default/salesforce"]) +def test_creator_rejects_too_short_object_qualified_name(value): + with pytest.raises(ValueError, match="object_qualified_name is invalid"): + SalesforceField.creator(**{**VALID, "object_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceField.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceField.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceField.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceField.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceField.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceField.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceField.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceField.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceField) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_object_test.py b/tests_v9/unit/model/salesforce_object_test.py new file mode 100644 index 000000000..a401adbcb --- /dev/null +++ b/tests_v9/unit/model/salesforce_object_test.py @@ -0,0 +1,108 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceObject.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_object, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceObject + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Account" +VALID = { + "name": NAME, + "api_name": "Account", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceObject.creator(**VALID) + + assert ( + sut.qualified_name == "default/salesforce/1712345678/00D5g000004XyZaEAK/Account" + ) + assert sut.name == NAME + assert sut.api_name == "Account" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +def test_creator_uses_api_name_not_label_in_qualified_name(): + sut = SalesforceObject.creator( + name="Custom Thing", api_name="My_Thing__c", organization_qualified_name=ORG_QN + ) + + assert sut.qualified_name == f"{ORG_QN}/My_Thing__c" + assert sut.name == "Custom Thing" + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceObject.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceObject.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceObject.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceObject.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceObject.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceObject.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceObject.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceObject.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceObject.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceObject) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_organization_test.py b/tests_v9/unit/model/salesforce_organization_test.py new file mode 100644 index 000000000..17b9a9c7c --- /dev/null +++ b/tests_v9/unit/model/salesforce_organization_test.py @@ -0,0 +1,85 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceOrganization.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_organization, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceOrganization + +CONNECTION_QN = "default/salesforce/1712345678" +NAME = "Acme Corp" +VALID = { + "name": NAME, + "connection_qualified_name": CONNECTION_QN, + "source_id": "00D5g000004XyZaEAK", +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceOrganization.creator(**VALID) + + assert sut.qualified_name == "default/salesforce/1712345678/00D5g000004XyZaEAK" + assert sut.name == NAME + assert sut.source_id == "00D5g000004XyZaEAK" + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +def test_validate_rejects_qualified_name_without_org_segment(): + sut = SalesforceOrganization.creator(**VALID) + sut.qualified_name = "00D5g000004XyZaEAK" + + with pytest.raises(ValueError, match="does not match expected pattern"): + sut.validate() + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceOrganization.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceOrganization.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceOrganization.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceOrganization.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceOrganization.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceOrganization.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceOrganization) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME diff --git a/tests_v9/unit/model/salesforce_report_test.py b/tests_v9/unit/model/salesforce_report_test.py new file mode 100644 index 000000000..06fa2bc4d --- /dev/null +++ b/tests_v9/unit/model/salesforce_report_test.py @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 Atlan Pte. Ltd. + +"""Unit tests for SalesforceReport.creator in pyatlan_v9. + +The expected qualifiedName is the exact string atlan-salesforce-app emits today +(app/asset_mapper.py map_report, FND-3838 census), so a creator change that would re-key +existing Salesforce assets fails here. +""" + +import pytest + +from pyatlan_v9.model import SalesforceReport + +CONNECTION_QN = "default/salesforce/1712345678" +ORG_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK" +NAME = "Quarterly Revenue" +VALID = { + "name": NAME, + "source_id": "00O5g000004AbCdEAF", + "organization_qualified_name": ORG_QN, +} +EXPECTED_QN = "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" + + +def test_creator_matches_app_qualified_name(): + sut = SalesforceReport.creator(**VALID) + + assert ( + sut.qualified_name + == "default/salesforce/1712345678/00D5g000004XyZaEAK/00O5g000004AbCdEAF" + ) + assert sut.name == NAME + assert sut.source_id == "00O5g000004AbCdEAF" + assert sut.organization_qualified_name == ORG_QN + assert sut.organization.qualified_name == ORG_QN + assert sut._QUALIFIED_NAME_PATTERN.match(sut.qualified_name) + sut.validate(for_creation=True) + + +@pytest.mark.parametrize("value", [CONNECTION_QN, "default"]) +def test_creator_rejects_too_short_organization_qualified_name(value): + with pytest.raises(ValueError, match="organization_qualified_name is invalid"): + SalesforceReport.creator(**{**VALID, "organization_qualified_name": value}) + + +def test_creator_derives_connection_qualified_name(): + sut = SalesforceReport.creator(**VALID) + + assert sut.connection_qualified_name == CONNECTION_QN + assert sut.connector_name == "salesforce" + + +def test_creator_keeps_explicit_connection_qualified_name(): + sut = SalesforceReport.creator(**VALID, connection_qualified_name=CONNECTION_QN) + + assert sut.connection_qualified_name == CONNECTION_QN + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_missing_input(key): + with pytest.raises(ValueError, match=f"{key} is required"): + SalesforceReport.creator(**{**VALID, key: None}) + + +@pytest.mark.parametrize("key", list(VALID)) +def test_creator_rejects_blank_input(key): + with pytest.raises(ValueError, match=f"{key} cannot be blank"): + SalesforceReport.creator(**{**VALID, key: " "}) + + +def test_create_alias(): + assert SalesforceReport.create(**VALID).qualified_name == EXPECTED_QN + + +@pytest.mark.parametrize( + "qualified_name, name, message", + [ + ("", "x", "qualified_name is required"), + (EXPECTED_QN, "", "name is required"), + ], +) +def test_updater_rejects_missing_input(qualified_name, name, message): + with pytest.raises(ValueError, match=message): + SalesforceReport.updater(qualified_name=qualified_name, name=name) + + +def test_updater(): + sut = SalesforceReport.updater(qualified_name=EXPECTED_QN, name=NAME) + + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME + + +def test_trim_to_required(): + sut = SalesforceReport.creator(**VALID).trim_to_required() + + assert isinstance(sut, SalesforceReport) + assert sut.qualified_name == EXPECTED_QN + assert sut.name == NAME