cumulusci-plus 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of cumulusci-plus might be problematic. Click here for more details.
- cumulusci/__about__.py +1 -0
- cumulusci/__init__.py +22 -0
- cumulusci/__main__.py +3 -0
- cumulusci/cli/__init__.py +0 -0
- cumulusci/cli/cci.py +244 -0
- cumulusci/cli/error.py +125 -0
- cumulusci/cli/flow.py +185 -0
- cumulusci/cli/logger.py +72 -0
- cumulusci/cli/org.py +692 -0
- cumulusci/cli/plan.py +181 -0
- cumulusci/cli/project.py +391 -0
- cumulusci/cli/robot.py +116 -0
- cumulusci/cli/runtime.py +190 -0
- cumulusci/cli/service.py +521 -0
- cumulusci/cli/task.py +295 -0
- cumulusci/cli/tests/__init__.py +0 -0
- cumulusci/cli/tests/test_cci.py +545 -0
- cumulusci/cli/tests/test_error.py +170 -0
- cumulusci/cli/tests/test_flow.py +276 -0
- cumulusci/cli/tests/test_logger.py +25 -0
- cumulusci/cli/tests/test_org.py +1438 -0
- cumulusci/cli/tests/test_plan.py +245 -0
- cumulusci/cli/tests/test_project.py +235 -0
- cumulusci/cli/tests/test_robot.py +177 -0
- cumulusci/cli/tests/test_runtime.py +197 -0
- cumulusci/cli/tests/test_service.py +853 -0
- cumulusci/cli/tests/test_task.py +266 -0
- cumulusci/cli/tests/test_ui.py +310 -0
- cumulusci/cli/tests/test_utils.py +122 -0
- cumulusci/cli/tests/utils.py +52 -0
- cumulusci/cli/ui.py +234 -0
- cumulusci/cli/utils.py +150 -0
- cumulusci/conftest.py +181 -0
- cumulusci/core/__init__.py +0 -0
- cumulusci/core/config/BaseConfig.py +5 -0
- cumulusci/core/config/BaseTaskFlowConfig.py +5 -0
- cumulusci/core/config/OrgConfig.py +5 -0
- cumulusci/core/config/ScratchOrgConfig.py +5 -0
- cumulusci/core/config/__init__.py +125 -0
- cumulusci/core/config/base_config.py +111 -0
- cumulusci/core/config/base_task_flow_config.py +82 -0
- cumulusci/core/config/marketing_cloud_service_config.py +83 -0
- cumulusci/core/config/oauth2_service_config.py +17 -0
- cumulusci/core/config/org_config.py +604 -0
- cumulusci/core/config/project_config.py +782 -0
- cumulusci/core/config/scratch_org_config.py +251 -0
- cumulusci/core/config/sfdx_org_config.py +220 -0
- cumulusci/core/config/tests/_test_config_backwards_compatibility.py +33 -0
- cumulusci/core/config/tests/test_config.py +1895 -0
- cumulusci/core/config/tests/test_config_expensive.py +839 -0
- cumulusci/core/config/tests/test_config_util.py +91 -0
- cumulusci/core/config/universal_config.py +88 -0
- cumulusci/core/config/util.py +18 -0
- cumulusci/core/datasets.py +303 -0
- cumulusci/core/debug.py +33 -0
- cumulusci/core/dependencies/__init__.py +55 -0
- cumulusci/core/dependencies/base.py +561 -0
- cumulusci/core/dependencies/dependencies.py +273 -0
- cumulusci/core/dependencies/github.py +177 -0
- cumulusci/core/dependencies/github_resolvers.py +244 -0
- cumulusci/core/dependencies/resolvers.py +580 -0
- cumulusci/core/dependencies/tests/__init__.py +0 -0
- cumulusci/core/dependencies/tests/conftest.py +385 -0
- cumulusci/core/dependencies/tests/test_dependencies.py +950 -0
- cumulusci/core/dependencies/tests/test_github.py +83 -0
- cumulusci/core/dependencies/tests/test_resolvers.py +1027 -0
- cumulusci/core/dependencies/utils.py +13 -0
- cumulusci/core/enums.py +11 -0
- cumulusci/core/exceptions.py +311 -0
- cumulusci/core/flowrunner.py +888 -0
- cumulusci/core/github.py +665 -0
- cumulusci/core/keychain/__init__.py +24 -0
- cumulusci/core/keychain/base_project_keychain.py +441 -0
- cumulusci/core/keychain/encrypted_file_project_keychain.py +945 -0
- cumulusci/core/keychain/environment_project_keychain.py +7 -0
- cumulusci/core/keychain/serialization.py +152 -0
- cumulusci/core/keychain/subprocess_keychain.py +24 -0
- cumulusci/core/keychain/tests/conftest.py +50 -0
- cumulusci/core/keychain/tests/test_base_project_keychain.py +299 -0
- cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +1228 -0
- cumulusci/core/metadeploy/__init__.py +0 -0
- cumulusci/core/metadeploy/api.py +88 -0
- cumulusci/core/metadeploy/plans.py +25 -0
- cumulusci/core/metadeploy/tests/test_api.py +276 -0
- cumulusci/core/runtime.py +115 -0
- cumulusci/core/sfdx.py +162 -0
- cumulusci/core/source/__init__.py +16 -0
- cumulusci/core/source/github.py +50 -0
- cumulusci/core/source/local_folder.py +35 -0
- cumulusci/core/source_transforms/__init__.py +0 -0
- cumulusci/core/source_transforms/tests/test_transforms.py +1091 -0
- cumulusci/core/source_transforms/transforms.py +532 -0
- cumulusci/core/tasks.py +404 -0
- cumulusci/core/template_utils.py +59 -0
- cumulusci/core/tests/__init__.py +0 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_e2e.yaml +215 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_extract_standard_objects.yaml +199 -0
- cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_read_explicit_extract_declaration.yaml +3 -0
- cumulusci/core/tests/fake_remote_repo/cumulusci.yml +32 -0
- cumulusci/core/tests/fake_remote_repo/tasks/directory/example_2.py +6 -0
- cumulusci/core/tests/fake_remote_repo/tasks/example.py +43 -0
- cumulusci/core/tests/fake_remote_repo_2/cumulusci.yml +11 -0
- cumulusci/core/tests/fake_remote_repo_2/tasks/example_3.py +6 -0
- cumulusci/core/tests/test_datasets_e2e.py +386 -0
- cumulusci/core/tests/test_exceptions.py +11 -0
- cumulusci/core/tests/test_flowrunner.py +836 -0
- cumulusci/core/tests/test_github.py +942 -0
- cumulusci/core/tests/test_sfdx.py +138 -0
- cumulusci/core/tests/test_source.py +678 -0
- cumulusci/core/tests/test_tasks.py +262 -0
- cumulusci/core/tests/test_utils.py +141 -0
- cumulusci/core/tests/test_utils_merge_config.py +276 -0
- cumulusci/core/tests/test_versions.py +76 -0
- cumulusci/core/tests/untrusted_repo_child/cumulusci.yml +7 -0
- cumulusci/core/tests/untrusted_repo_child/tasks/untrusted_child.py +6 -0
- cumulusci/core/tests/untrusted_repo_parent/cumulusci.yml +26 -0
- cumulusci/core/tests/untrusted_repo_parent/tasks/untrusted_parent.py +6 -0
- cumulusci/core/tests/utils.py +116 -0
- cumulusci/core/tests/yaml/global.yaml +0 -0
- cumulusci/core/utils.py +402 -0
- cumulusci/core/versions.py +149 -0
- cumulusci/cumulusci.yml +1621 -0
- cumulusci/files/admin_profile.xml +20 -0
- cumulusci/files/delete_excludes.txt +424 -0
- cumulusci/files/templates/project/README.md +12 -0
- cumulusci/files/templates/project/cumulusci.yml +63 -0
- cumulusci/files/templates/project/dot-gitignore +60 -0
- cumulusci/files/templates/project/mapping.yml +45 -0
- cumulusci/files/templates/project/scratch_def.json +25 -0
- cumulusci/oauth/__init__.py +0 -0
- cumulusci/oauth/client.py +400 -0
- cumulusci/oauth/exceptions.py +9 -0
- cumulusci/oauth/salesforce.py +95 -0
- cumulusci/oauth/tests/__init__.py +0 -0
- cumulusci/oauth/tests/cassettes/test_get_device_code.yaml +22 -0
- cumulusci/oauth/tests/cassettes/test_get_device_oauth_token.yaml +74 -0
- cumulusci/oauth/tests/test_client.py +308 -0
- cumulusci/oauth/tests/test_salesforce.py +46 -0
- cumulusci/plugins/__init__.py +3 -0
- cumulusci/plugins/plugin_base.py +93 -0
- cumulusci/plugins/plugin_loader.py +59 -0
- cumulusci/robotframework/CumulusCI.py +340 -0
- cumulusci/robotframework/CumulusCI.robot +7 -0
- cumulusci/robotframework/Performance.py +165 -0
- cumulusci/robotframework/Salesforce.py +936 -0
- cumulusci/robotframework/Salesforce.robot +192 -0
- cumulusci/robotframework/SalesforceAPI.py +416 -0
- cumulusci/robotframework/SalesforcePlaywright.py +220 -0
- cumulusci/robotframework/SalesforcePlaywright.robot +40 -0
- cumulusci/robotframework/__init__.py +2 -0
- cumulusci/robotframework/base_library.py +39 -0
- cumulusci/robotframework/faker_mixin.py +89 -0
- cumulusci/robotframework/form_handlers.py +222 -0
- cumulusci/robotframework/javascript/cci_init.js +34 -0
- cumulusci/robotframework/javascript/cumulusci.js +4 -0
- cumulusci/robotframework/locator_manager.py +197 -0
- cumulusci/robotframework/locators_56.py +88 -0
- cumulusci/robotframework/locators_57.py +5 -0
- cumulusci/robotframework/pageobjects/BasePageObjects.py +433 -0
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +246 -0
- cumulusci/robotframework/pageobjects/PageObjectLibrary.py +45 -0
- cumulusci/robotframework/pageobjects/PageObjects.py +351 -0
- cumulusci/robotframework/pageobjects/__init__.py +12 -0
- cumulusci/robotframework/pageobjects/baseobjects.py +120 -0
- cumulusci/robotframework/perftests/short/collection_perf.robot +105 -0
- cumulusci/robotframework/tests/CustomObjectTestPage.py +10 -0
- cumulusci/robotframework/tests/FooTestPage.py +8 -0
- cumulusci/robotframework/tests/cumulusci/base.robot +40 -0
- cumulusci/robotframework/tests/cumulusci/bulkdata.robot +38 -0
- cumulusci/robotframework/tests/cumulusci/communities.robot +57 -0
- cumulusci/robotframework/tests/cumulusci/datagen.robot +84 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryA.py +24 -0
- cumulusci/robotframework/tests/salesforce/TestLibraryB.py +20 -0
- cumulusci/robotframework/tests/salesforce/TestListener.py +93 -0
- cumulusci/robotframework/tests/salesforce/api.robot +178 -0
- cumulusci/robotframework/tests/salesforce/browsers.robot +143 -0
- cumulusci/robotframework/tests/salesforce/classic.robot +51 -0
- cumulusci/robotframework/tests/salesforce/create_contact.robot +59 -0
- cumulusci/robotframework/tests/salesforce/faker.robot +68 -0
- cumulusci/robotframework/tests/salesforce/forms.robot +172 -0
- cumulusci/robotframework/tests/salesforce/label_locator.robot +244 -0
- cumulusci/robotframework/tests/salesforce/labels.html +33 -0
- cumulusci/robotframework/tests/salesforce/locators.robot +149 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/base_pageobjects.robot +100 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/example_page_object.py +25 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/listing_page.robot +115 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/objectmanager.robot +74 -0
- cumulusci/robotframework/tests/salesforce/pageobjects/pageobjects.robot +171 -0
- cumulusci/robotframework/tests/salesforce/performance.robot +109 -0
- cumulusci/robotframework/tests/salesforce/playwright/javascript_keywords.robot +33 -0
- cumulusci/robotframework/tests/salesforce/playwright/open_test_browser.robot +48 -0
- cumulusci/robotframework/tests/salesforce/playwright/playwright.robot +24 -0
- cumulusci/robotframework/tests/salesforce/playwright/ui.robot +32 -0
- cumulusci/robotframework/tests/salesforce/populate.robot +89 -0
- cumulusci/robotframework/tests/salesforce/test_testlistener.py +37 -0
- cumulusci/robotframework/tests/salesforce/ui.robot +361 -0
- cumulusci/robotframework/tests/test_cumulusci_library.py +304 -0
- cumulusci/robotframework/tests/test_locator_manager.py +158 -0
- cumulusci/robotframework/tests/test_pageobjects.py +291 -0
- cumulusci/robotframework/tests/test_performance.py +38 -0
- cumulusci/robotframework/tests/test_salesforce.py +79 -0
- cumulusci/robotframework/tests/test_salesforce_locators.py +73 -0
- cumulusci/robotframework/tests/test_template_util.py +53 -0
- cumulusci/robotframework/tests/test_utils.py +106 -0
- cumulusci/robotframework/utils.py +283 -0
- cumulusci/salesforce_api/__init__.py +0 -0
- cumulusci/salesforce_api/exceptions.py +23 -0
- cumulusci/salesforce_api/filterable_objects.py +96 -0
- cumulusci/salesforce_api/mc_soap_envelopes.py +89 -0
- cumulusci/salesforce_api/metadata.py +721 -0
- cumulusci/salesforce_api/org_schema.py +571 -0
- cumulusci/salesforce_api/org_schema_models.py +226 -0
- cumulusci/salesforce_api/package_install.py +265 -0
- cumulusci/salesforce_api/package_zip.py +301 -0
- cumulusci/salesforce_api/rest_deploy.py +148 -0
- cumulusci/salesforce_api/retrieve_profile_api.py +301 -0
- cumulusci/salesforce_api/soap_envelopes.py +177 -0
- cumulusci/salesforce_api/tests/__init__.py +0 -0
- cumulusci/salesforce_api/tests/metadata_test_strings.py +24 -0
- cumulusci/salesforce_api/tests/test_metadata.py +1015 -0
- cumulusci/salesforce_api/tests/test_package_install.py +219 -0
- cumulusci/salesforce_api/tests/test_package_zip.py +380 -0
- cumulusci/salesforce_api/tests/test_rest_deploy.py +264 -0
- cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +337 -0
- cumulusci/salesforce_api/tests/test_utils.py +124 -0
- cumulusci/salesforce_api/utils.py +51 -0
- cumulusci/schema/cumulusci.jsonschema.json +782 -0
- cumulusci/tasks/__init__.py +0 -0
- cumulusci/tasks/apex/__init__.py +0 -0
- cumulusci/tasks/apex/anon.py +157 -0
- cumulusci/tasks/apex/batch.py +180 -0
- cumulusci/tasks/apex/testrunner.py +835 -0
- cumulusci/tasks/apex/tests/cassettes/ManualEditTestApexIntegrationTests.test_run_tests__integration_test.yaml +703 -0
- cumulusci/tasks/apex/tests/test_apex_tasks.py +1558 -0
- cumulusci/tasks/base_source_control_task.py +17 -0
- cumulusci/tasks/bulkdata/__init__.py +15 -0
- cumulusci/tasks/bulkdata/base_generate_data_task.py +96 -0
- cumulusci/tasks/bulkdata/dates.py +97 -0
- cumulusci/tasks/bulkdata/delete.py +156 -0
- cumulusci/tasks/bulkdata/extract.py +441 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/calculate_dependencies.py +117 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +123 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/hardcoded_default_declarations.py +49 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +283 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +142 -0
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +575 -0
- cumulusci/tasks/bulkdata/factory_utils.py +134 -0
- cumulusci/tasks/bulkdata/generate.py +4 -0
- cumulusci/tasks/bulkdata/generate_and_load_data.py +232 -0
- cumulusci/tasks/bulkdata/generate_and_load_data_from_yaml.py +19 -0
- cumulusci/tasks/bulkdata/generate_from_yaml.py +183 -0
- cumulusci/tasks/bulkdata/generate_mapping.py +434 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +169 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py +45 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py +121 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/load_mapping_file_generator.py +127 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py +53 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_transforms.py +139 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +135 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_load_mapping_from_declarations.py +330 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_generator_post_processes.py +60 -0
- cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_transforms.py +188 -0
- cumulusci/tasks/bulkdata/load.py +1196 -0
- cumulusci/tasks/bulkdata/mapping_parser.py +811 -0
- cumulusci/tasks/bulkdata/query_transformers.py +264 -0
- cumulusci/tasks/bulkdata/select_utils.py +792 -0
- cumulusci/tasks/bulkdata/snowfakery.py +753 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/queue_manager.py +478 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_run_until.py +141 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py +53 -0
- cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py +64 -0
- cumulusci/tasks/bulkdata/step.py +1242 -0
- cumulusci/tasks/bulkdata/tests/__init__.py +0 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_random_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_annoy_strategy.yaml +123 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy.yaml +313 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy_bulk.yaml +550 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_strategy.yaml +175 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_standard_strategy.yaml +147 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__multiple_needed.yaml +69 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__none_needed.yaml +22 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__one_needed.yaml +24 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_snowfakery_query_salesforce.yaml +25 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpdatesIntegrationTests.test_updates_task.yaml +80 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_simple_upsert__rest.yaml +270 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert__rest.yaml +267 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field__rest.yaml +369 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field_rest__duplicate_error.yaml +204 -0
- cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_fields__bulk.yaml +675 -0
- cumulusci/tasks/bulkdata/tests/dummy_data_factory.py +36 -0
- cumulusci/tasks/bulkdata/tests/integration_test_utils.py +49 -0
- cumulusci/tasks/bulkdata/tests/mapping-oid.yml +87 -0
- cumulusci/tasks/bulkdata/tests/mapping_after.yml +38 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly.yml +34 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_incomplete.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_poly_wrong.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_strategy.yml +20 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_number.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_strategy.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__non_float.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_missing_priority_fields.yml +22 -0
- cumulusci/tasks/bulkdata/tests/mapping_select_no_priority_fields.yml +18 -0
- cumulusci/tasks/bulkdata/tests/mapping_simple.yml +27 -0
- cumulusci/tasks/bulkdata/tests/mapping_v1.yml +28 -0
- cumulusci/tasks/bulkdata/tests/mapping_v2.yml +21 -0
- cumulusci/tasks/bulkdata/tests/mapping_v3.yml +32 -0
- cumulusci/tasks/bulkdata/tests/mapping_vanilla_sf.yml +69 -0
- cumulusci/tasks/bulkdata/tests/mock_data_factory_without_mapping.py +12 -0
- cumulusci/tasks/bulkdata/tests/person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/person_accounts_minimal.yml +15 -0
- cumulusci/tasks/bulkdata/tests/recordtypes.yml +8 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_2.yml +6 -0
- cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml +8 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child/child2.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/child.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/gen_npsp_standard_objects.recipe.yml +89 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/include_parent.yml +3 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/npsp_standard_objects_macros.yml +34 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/options.recipe.yml +6 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +16 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/sf_standard_object_macros.yml +83 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.load.yml +2 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.recipe.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_2.load.yml +5 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.recipe.yml +12 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels_2.load.yml +13 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/unique_values.recipe.yml +4 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert.recipe.yml +23 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_2.recipe.yml +29 -0
- cumulusci/tasks/bulkdata/tests/snowfakery/upsert_before.yml +10 -0
- cumulusci/tasks/bulkdata/tests/test_base_generate_data_tasks.py +61 -0
- cumulusci/tasks/bulkdata/tests/test_dates.py +99 -0
- cumulusci/tasks/bulkdata/tests/test_delete.py +404 -0
- cumulusci/tasks/bulkdata/tests/test_extract.py +1311 -0
- cumulusci/tasks/bulkdata/tests/test_factory_utils.py +55 -0
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +252 -0
- cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +343 -0
- cumulusci/tasks/bulkdata/tests/test_generatemapping.py +1039 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +3175 -0
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +1658 -0
- cumulusci/tasks/bulkdata/tests/test_query_db__joins_self_lookups.yml +12 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups.yml +26 -0
- cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups_select.yml +48 -0
- cumulusci/tasks/bulkdata/tests/test_select.py +171 -0
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +1057 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +1153 -0
- cumulusci/tasks/bulkdata/tests/test_step.py +3957 -0
- cumulusci/tasks/bulkdata/tests/test_updates.py +513 -0
- cumulusci/tasks/bulkdata/tests/test_upsert.py +1015 -0
- cumulusci/tasks/bulkdata/tests/test_utils.py +158 -0
- cumulusci/tasks/bulkdata/tests/testdata.db +0 -0
- cumulusci/tasks/bulkdata/tests/update_describe.py +50 -0
- cumulusci/tasks/bulkdata/tests/update_person_accounts.yml +23 -0
- cumulusci/tasks/bulkdata/tests/utils.py +114 -0
- cumulusci/tasks/bulkdata/update_data.py +260 -0
- cumulusci/tasks/bulkdata/upsert_utils.py +130 -0
- cumulusci/tasks/bulkdata/utils.py +249 -0
- cumulusci/tasks/command.py +178 -0
- cumulusci/tasks/connectedapp.py +186 -0
- cumulusci/tasks/create_package_version.py +778 -0
- cumulusci/tasks/datadictionary.py +745 -0
- cumulusci/tasks/dx_convert_from.py +26 -0
- cumulusci/tasks/github/__init__.py +17 -0
- cumulusci/tasks/github/base.py +16 -0
- cumulusci/tasks/github/commit_status.py +13 -0
- cumulusci/tasks/github/merge.py +11 -0
- cumulusci/tasks/github/publish.py +11 -0
- cumulusci/tasks/github/pull_request.py +11 -0
- cumulusci/tasks/github/release.py +11 -0
- cumulusci/tasks/github/release_report.py +11 -0
- cumulusci/tasks/github/tag.py +11 -0
- cumulusci/tasks/github/tests/__init__.py +0 -0
- cumulusci/tasks/github/tests/test_util.py +202 -0
- cumulusci/tasks/github/tests/test_vcs_migration.py +44 -0
- cumulusci/tasks/github/tests/util_github_api.py +666 -0
- cumulusci/tasks/github/util.py +252 -0
- cumulusci/tasks/marketing_cloud/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/api.py +188 -0
- cumulusci/tasks/marketing_cloud/base.py +38 -0
- cumulusci/tasks/marketing_cloud/deploy.py +345 -0
- cumulusci/tasks/marketing_cloud/get_user_info.py +40 -0
- cumulusci/tasks/marketing_cloud/mc_constants.py +1 -0
- cumulusci/tasks/marketing_cloud/tests/__init__.py +0 -0
- cumulusci/tasks/marketing_cloud/tests/conftest.py +46 -0
- cumulusci/tasks/marketing_cloud/tests/expected-payload.json +110 -0
- cumulusci/tasks/marketing_cloud/tests/test_api.py +97 -0
- cumulusci/tasks/marketing_cloud/tests/test_api_soap_envelopes.py +145 -0
- cumulusci/tasks/marketing_cloud/tests/test_base.py +14 -0
- cumulusci/tasks/marketing_cloud/tests/test_deploy.py +400 -0
- cumulusci/tasks/marketing_cloud/tests/test_get_user_info.py +141 -0
- cumulusci/tasks/marketing_cloud/tests/validation-response.json +39 -0
- cumulusci/tasks/metadata/__init__.py +0 -0
- cumulusci/tasks/metadata/ee_src.py +94 -0
- cumulusci/tasks/metadata/managed_src.py +100 -0
- cumulusci/tasks/metadata/metadata_map.yml +868 -0
- cumulusci/tasks/metadata/modify.py +99 -0
- cumulusci/tasks/metadata/package.py +684 -0
- cumulusci/tasks/metadata/tests/__init__.py +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/.hidden/.keep +0 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/destructiveChanges.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package.xml +9 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package_install_uninstall.xml +11 -0
- cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/reports/namespace__TestFolder/TestReport.report +3 -0
- cumulusci/tasks/metadata/tests/sample_package.xml +9 -0
- cumulusci/tasks/metadata/tests/test_ee_src.py +112 -0
- cumulusci/tasks/metadata/tests/test_managed_src.py +111 -0
- cumulusci/tasks/metadata/tests/test_modify.py +123 -0
- cumulusci/tasks/metadata/tests/test_package.py +476 -0
- cumulusci/tasks/metadata_etl/__init__.py +29 -0
- cumulusci/tasks/metadata_etl/base.py +436 -0
- cumulusci/tasks/metadata_etl/duplicate_rules.py +24 -0
- cumulusci/tasks/metadata_etl/field_sets.py +70 -0
- cumulusci/tasks/metadata_etl/help_text.py +92 -0
- cumulusci/tasks/metadata_etl/layouts.py +550 -0
- cumulusci/tasks/metadata_etl/objects.py +68 -0
- cumulusci/tasks/metadata_etl/permissions.py +167 -0
- cumulusci/tasks/metadata_etl/picklists.py +221 -0
- cumulusci/tasks/metadata_etl/remote_site_settings.py +99 -0
- cumulusci/tasks/metadata_etl/sharing.py +138 -0
- cumulusci/tasks/metadata_etl/tests/test_base.py +512 -0
- cumulusci/tasks/metadata_etl/tests/test_duplicate_rules.py +22 -0
- cumulusci/tasks/metadata_etl/tests/test_field_sets.py +156 -0
- cumulusci/tasks/metadata_etl/tests/test_help_text.py +387 -0
- cumulusci/tasks/metadata_etl/tests/test_ip_ranges.py +85 -0
- cumulusci/tasks/metadata_etl/tests/test_layouts.py +858 -0
- cumulusci/tasks/metadata_etl/tests/test_objects.py +236 -0
- cumulusci/tasks/metadata_etl/tests/test_permissions.py +223 -0
- cumulusci/tasks/metadata_etl/tests/test_picklists.py +547 -0
- cumulusci/tasks/metadata_etl/tests/test_remote_site_settings.py +46 -0
- cumulusci/tasks/metadata_etl/tests/test_sharing.py +333 -0
- cumulusci/tasks/metadata_etl/tests/test_value_sets.py +298 -0
- cumulusci/tasks/metadata_etl/value_sets.py +106 -0
- cumulusci/tasks/metadeploy.py +393 -0
- cumulusci/tasks/metaxml.py +88 -0
- cumulusci/tasks/preflight/__init__.py +0 -0
- cumulusci/tasks/preflight/dataset_load.py +49 -0
- cumulusci/tasks/preflight/licenses.py +86 -0
- cumulusci/tasks/preflight/packages.py +14 -0
- cumulusci/tasks/preflight/permsets.py +23 -0
- cumulusci/tasks/preflight/recordtypes.py +16 -0
- cumulusci/tasks/preflight/retrieve_tasks.py +30 -0
- cumulusci/tasks/preflight/settings.py +77 -0
- cumulusci/tasks/preflight/sobjects.py +202 -0
- cumulusci/tasks/preflight/tests/test_dataset_load.py +85 -0
- cumulusci/tasks/preflight/tests/test_licenses.py +174 -0
- cumulusci/tasks/preflight/tests/test_packages.py +14 -0
- cumulusci/tasks/preflight/tests/test_permset_preflights.py +51 -0
- cumulusci/tasks/preflight/tests/test_recordtypes.py +30 -0
- cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +62 -0
- cumulusci/tasks/preflight/tests/test_settings.py +130 -0
- cumulusci/tasks/preflight/tests/test_sobjects.py +231 -0
- cumulusci/tasks/push/README.md +59 -0
- cumulusci/tasks/push/__init__.py +0 -0
- cumulusci/tasks/push/push_api.py +659 -0
- cumulusci/tasks/push/pushfails.py +136 -0
- cumulusci/tasks/push/tasks.py +476 -0
- cumulusci/tasks/push/tests/conftest.py +263 -0
- cumulusci/tasks/push/tests/test_push_api.py +951 -0
- cumulusci/tasks/push/tests/test_push_tasks.py +659 -0
- cumulusci/tasks/release_notes/README.md +63 -0
- cumulusci/tasks/release_notes/__init__.py +0 -0
- cumulusci/tasks/release_notes/exceptions.py +5 -0
- cumulusci/tasks/release_notes/generator.py +137 -0
- cumulusci/tasks/release_notes/parser.py +232 -0
- cumulusci/tasks/release_notes/provider.py +44 -0
- cumulusci/tasks/release_notes/task.py +300 -0
- cumulusci/tasks/release_notes/tests/__init__.py +0 -0
- cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +17 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/2.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/multi/3.txt +1 -0
- cumulusci/tasks/release_notes/tests/change_notes/single/1.txt +1 -0
- cumulusci/tasks/release_notes/tests/test_generator.py +582 -0
- cumulusci/tasks/release_notes/tests/test_parser.py +867 -0
- cumulusci/tasks/release_notes/tests/test_provider.py +512 -0
- cumulusci/tasks/release_notes/tests/test_task.py +461 -0
- cumulusci/tasks/release_notes/tests/utils.py +153 -0
- cumulusci/tasks/robotframework/__init__.py +3 -0
- cumulusci/tasks/robotframework/debugger/DebugListener.py +100 -0
- cumulusci/tasks/robotframework/debugger/__init__.py +10 -0
- cumulusci/tasks/robotframework/debugger/model.py +87 -0
- cumulusci/tasks/robotframework/debugger/ui.py +259 -0
- cumulusci/tasks/robotframework/libdoc.py +269 -0
- cumulusci/tasks/robotframework/robotframework.py +392 -0
- cumulusci/tasks/robotframework/stylesheet.css +130 -0
- cumulusci/tasks/robotframework/template.html +109 -0
- cumulusci/tasks/robotframework/tests/TestLibrary.py +18 -0
- cumulusci/tasks/robotframework/tests/TestPageObjects.py +31 -0
- cumulusci/tasks/robotframework/tests/TestResource.robot +8 -0
- cumulusci/tasks/robotframework/tests/failing_tests.robot +16 -0
- cumulusci/tasks/robotframework/tests/performance.robot +23 -0
- cumulusci/tasks/robotframework/tests/test_browser_proxies.py +137 -0
- cumulusci/tasks/robotframework/tests/test_debugger.py +360 -0
- cumulusci/tasks/robotframework/tests/test_robot_parallel.py +141 -0
- cumulusci/tasks/robotframework/tests/test_robotframework.py +860 -0
- cumulusci/tasks/salesforce/BaseRetrieveMetadata.py +58 -0
- cumulusci/tasks/salesforce/BaseSalesforceApiTask.py +45 -0
- cumulusci/tasks/salesforce/BaseSalesforceMetadataApiTask.py +18 -0
- cumulusci/tasks/salesforce/BaseSalesforceTask.py +4 -0
- cumulusci/tasks/salesforce/BaseUninstallMetadata.py +41 -0
- cumulusci/tasks/salesforce/CreateCommunity.py +124 -0
- cumulusci/tasks/salesforce/CreatePackage.py +29 -0
- cumulusci/tasks/salesforce/Deploy.py +240 -0
- cumulusci/tasks/salesforce/DeployBundles.py +88 -0
- cumulusci/tasks/salesforce/DescribeMetadataTypes.py +26 -0
- cumulusci/tasks/salesforce/EnsureRecordTypes.py +202 -0
- cumulusci/tasks/salesforce/GetInstalledPackages.py +8 -0
- cumulusci/tasks/salesforce/ListCommunities.py +40 -0
- cumulusci/tasks/salesforce/ListCommunityTemplates.py +19 -0
- cumulusci/tasks/salesforce/PublishCommunity.py +62 -0
- cumulusci/tasks/salesforce/RetrievePackaged.py +41 -0
- cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +82 -0
- cumulusci/tasks/salesforce/RetrieveUnpackaged.py +36 -0
- cumulusci/tasks/salesforce/SOQLQuery.py +39 -0
- cumulusci/tasks/salesforce/UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/UninstallLocalBundles.py +28 -0
- cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +58 -0
- cumulusci/tasks/salesforce/UninstallPackage.py +32 -0
- cumulusci/tasks/salesforce/UninstallPackaged.py +56 -0
- cumulusci/tasks/salesforce/UpdateAdminProfile.py +8 -0
- cumulusci/tasks/salesforce/__init__.py +79 -0
- cumulusci/tasks/salesforce/activate_flow.py +74 -0
- cumulusci/tasks/salesforce/check_components.py +324 -0
- cumulusci/tasks/salesforce/composite.py +142 -0
- cumulusci/tasks/salesforce/create_permission_sets.py +35 -0
- cumulusci/tasks/salesforce/custom_settings.py +134 -0
- cumulusci/tasks/salesforce/custom_settings_wait.py +132 -0
- cumulusci/tasks/salesforce/enable_prediction.py +107 -0
- cumulusci/tasks/salesforce/insert_record.py +40 -0
- cumulusci/tasks/salesforce/install_package_version.py +242 -0
- cumulusci/tasks/salesforce/license_preflights.py +8 -0
- cumulusci/tasks/salesforce/network_member_group.py +178 -0
- cumulusci/tasks/salesforce/nonsourcetracking.py +228 -0
- cumulusci/tasks/salesforce/org_settings.py +193 -0
- cumulusci/tasks/salesforce/package_upload.py +328 -0
- cumulusci/tasks/salesforce/profiles.py +74 -0
- cumulusci/tasks/salesforce/promote_package_version.py +376 -0
- cumulusci/tasks/salesforce/retrieve_profile.py +195 -0
- cumulusci/tasks/salesforce/salesforce_files.py +244 -0
- cumulusci/tasks/salesforce/sourcetracking.py +507 -0
- cumulusci/tasks/salesforce/tests/__init__.py +3 -0
- cumulusci/tasks/salesforce/tests/test_CreateCommunity.py +278 -0
- cumulusci/tasks/salesforce/tests/test_CreatePackage.py +22 -0
- cumulusci/tasks/salesforce/tests/test_Deploy.py +470 -0
- cumulusci/tasks/salesforce/tests/test_DeployBundles.py +76 -0
- cumulusci/tasks/salesforce/tests/test_EnsureRecordTypes.py +345 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunities.py +84 -0
- cumulusci/tasks/salesforce/tests/test_ListCommunityTemplates.py +49 -0
- cumulusci/tasks/salesforce/tests/test_PackageUpload.py +547 -0
- cumulusci/tasks/salesforce/tests/test_ProfileGrantAllAccess.py +699 -0
- cumulusci/tasks/salesforce/tests/test_PublishCommunity.py +181 -0
- cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py +24 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py +56 -0
- cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py +21 -0
- cumulusci/tasks/salesforce/tests/test_SOQLQuery.py +30 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocal.py +15 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py +22 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackage.py +19 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackaged.py +66 -0
- cumulusci/tasks/salesforce/tests/test_UninstallPackagedIncremental.py +127 -0
- cumulusci/tasks/salesforce/tests/test_activate_flow.py +132 -0
- cumulusci/tasks/salesforce/tests/test_base_tasks.py +110 -0
- cumulusci/tasks/salesforce/tests/test_check_components.py +445 -0
- cumulusci/tasks/salesforce/tests/test_composite.py +250 -0
- cumulusci/tasks/salesforce/tests/test_create_permission_sets.py +41 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings.py +227 -0
- cumulusci/tasks/salesforce/tests/test_custom_settings_wait.py +174 -0
- cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +18 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +240 -0
- cumulusci/tasks/salesforce/tests/test_insert_record.py +110 -0
- cumulusci/tasks/salesforce/tests/test_install_package_version.py +464 -0
- cumulusci/tasks/salesforce/tests/test_network_member_group.py +444 -0
- cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py +235 -0
- cumulusci/tasks/salesforce/tests/test_org_settings.py +407 -0
- cumulusci/tasks/salesforce/tests/test_profiles.py +202 -0
- cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +287 -0
- cumulusci/tasks/salesforce/tests/test_salesforce_files.py +228 -0
- cumulusci/tasks/salesforce/tests/test_sourcetracking.py +350 -0
- cumulusci/tasks/salesforce/tests/test_trigger_handlers.py +300 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +509 -0
- cumulusci/tasks/salesforce/tests/util.py +79 -0
- cumulusci/tasks/salesforce/trigger_handlers.py +119 -0
- cumulusci/tasks/salesforce/uninstall_packaged_incremental.py +136 -0
- cumulusci/tasks/salesforce/update_dependencies.py +290 -0
- cumulusci/tasks/salesforce/update_profile.py +339 -0
- cumulusci/tasks/salesforce/users/permsets.py +227 -0
- cumulusci/tasks/salesforce/users/photos.py +162 -0
- cumulusci/tasks/salesforce/users/tests/photo.mock.txt +1 -0
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +950 -0
- cumulusci/tasks/salesforce/users/tests/test_photos.py +373 -0
- cumulusci/tasks/sample_data/capture_sample_data.py +77 -0
- cumulusci/tasks/sample_data/load_sample_data.py +85 -0
- cumulusci/tasks/sample_data/test_capture_sample_data.py +117 -0
- cumulusci/tasks/sample_data/test_load_sample_data.py +121 -0
- cumulusci/tasks/sfdx.py +83 -0
- cumulusci/tasks/tests/__init__.py +1 -0
- cumulusci/tasks/tests/conftest.py +30 -0
- cumulusci/tasks/tests/test_command.py +129 -0
- cumulusci/tasks/tests/test_connectedapp.py +236 -0
- cumulusci/tasks/tests/test_create_package_version.py +847 -0
- cumulusci/tasks/tests/test_datadictionary.py +1575 -0
- cumulusci/tasks/tests/test_dx_convert_from.py +60 -0
- cumulusci/tasks/tests/test_metadeploy.py +624 -0
- cumulusci/tasks/tests/test_metaxml.py +99 -0
- cumulusci/tasks/tests/test_promote_package_version.py +488 -0
- cumulusci/tasks/tests/test_pushfails.py +96 -0
- cumulusci/tasks/tests/test_salesforce.py +72 -0
- cumulusci/tasks/tests/test_sfdx.py +105 -0
- cumulusci/tasks/tests/test_util.py +207 -0
- cumulusci/tasks/util.py +261 -0
- cumulusci/tasks/vcs/__init__.py +19 -0
- cumulusci/tasks/vcs/commit_status.py +58 -0
- cumulusci/tasks/vcs/create_commit_status.py +37 -0
- cumulusci/tasks/vcs/download_extract.py +199 -0
- cumulusci/tasks/vcs/merge.py +298 -0
- cumulusci/tasks/vcs/publish.py +207 -0
- cumulusci/tasks/vcs/pull_request.py +9 -0
- cumulusci/tasks/vcs/release.py +134 -0
- cumulusci/tasks/vcs/release_report.py +105 -0
- cumulusci/tasks/vcs/tag.py +31 -0
- cumulusci/tasks/vcs/tests/github/test_commit_status.py +196 -0
- cumulusci/tasks/vcs/tests/github/test_download_extract.py +896 -0
- cumulusci/tasks/vcs/tests/github/test_merge.py +1118 -0
- cumulusci/tasks/vcs/tests/github/test_publish.py +823 -0
- cumulusci/tasks/vcs/tests/github/test_pull_request.py +29 -0
- cumulusci/tasks/vcs/tests/github/test_release.py +390 -0
- cumulusci/tasks/vcs/tests/github/test_release_report.py +109 -0
- cumulusci/tasks/vcs/tests/github/test_tag.py +90 -0
- cumulusci/tasks/vlocity/exceptions.py +2 -0
- cumulusci/tasks/vlocity/tests/test_vlocity.py +283 -0
- cumulusci/tasks/vlocity/vlocity.py +342 -0
- cumulusci/tests/__init__.py +1 -0
- cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml +18 -0
- cumulusci/tests/cassettes/TestIntegrationInfrastructure.test_integration_tests.yaml +19 -0
- cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +307 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr.py +275 -0
- cumulusci/tests/pytest_plugins/pytest_sf_vcr_serializer.py +160 -0
- cumulusci/tests/pytest_plugins/pytest_typeguard.py +5 -0
- cumulusci/tests/pytest_plugins/test_vcr_string_compressor.py +49 -0
- cumulusci/tests/pytest_plugins/vcr_string_compressor.py +97 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Account_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Case_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Contact_describe.yaml +4838 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Custom__c_describe.yaml +242 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Event_describe.yaml +19 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml +1338 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Lead_describe.yaml +18 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_OpportunityContactRole_describe.yaml +34 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Opportunity_describe.yaml +1261 -0
- cumulusci/tests/shared_cassettes/GET_sobjects_Organization.yaml +49 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl +15 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl +13 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl +24 -0
- cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl +25 -0
- cumulusci/tests/test_entry_points.py +20 -0
- cumulusci/tests/test_integration_infrastructure.py +131 -0
- cumulusci/tests/test_main.py +9 -0
- cumulusci/tests/test_schema.py +32 -0
- cumulusci/tests/test_utils.py +657 -0
- cumulusci/tests/test_vcr_serializer.py +134 -0
- cumulusci/tests/uncompressed_cassette.yaml +83 -0
- cumulusci/tests/util.py +344 -0
- cumulusci/utils/__init__.py +731 -0
- cumulusci/utils/classutils.py +9 -0
- cumulusci/utils/collections.py +32 -0
- cumulusci/utils/deprecation.py +11 -0
- cumulusci/utils/encryption.py +31 -0
- cumulusci/utils/fileutils.py +295 -0
- cumulusci/utils/git.py +142 -0
- cumulusci/utils/http/multi_request.py +214 -0
- cumulusci/utils/http/requests_utils.py +103 -0
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +32 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_composite_parallel_salesforce.yaml +65 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_errors.yaml +24 -0
- cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_reference_ids.yaml +49 -0
- cumulusci/utils/http/tests/test_multi_request.py +255 -0
- cumulusci/utils/iterators.py +21 -0
- cumulusci/utils/logging.py +128 -0
- cumulusci/utils/metaprogramming.py +10 -0
- cumulusci/utils/options.py +138 -0
- cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py +29 -0
- cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py +50 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +238 -0
- cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +243 -0
- cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +353 -0
- cumulusci/utils/salesforce/count_sobjects.py +46 -0
- cumulusci/utils/salesforce/soql.py +17 -0
- cumulusci/utils/salesforce/tests/cassettes/ManualEdit_TestCountSObjects.test_count_sobjects__network_errors.yaml +23 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects__errors.yaml +33 -0
- cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects_simple.yaml +29 -0
- cumulusci/utils/salesforce/tests/test_count_sobjects.py +29 -0
- cumulusci/utils/salesforce/tests/test_soql.py +30 -0
- cumulusci/utils/tests/cassettes/ManualEditTestDescribeOrg.test_minimal_schema.yaml +36 -0
- cumulusci/utils/tests/cassettes/ManualEdit_test_describe_to_sql.yaml +191 -0
- cumulusci/utils/tests/test_fileutils.py +284 -0
- cumulusci/utils/tests/test_git.py +85 -0
- cumulusci/utils/tests/test_logging.py +70 -0
- cumulusci/utils/tests/test_option_parsing.py +188 -0
- cumulusci/utils/tests/test_org_schema.py +691 -0
- cumulusci/utils/tests/test_org_schema_models.py +79 -0
- cumulusci/utils/tests/test_waiting.py +25 -0
- cumulusci/utils/version_strings.py +391 -0
- cumulusci/utils/waiting.py +42 -0
- cumulusci/utils/xml/__init__.py +91 -0
- cumulusci/utils/xml/metadata_tree.py +299 -0
- cumulusci/utils/xml/robot_xml.py +114 -0
- cumulusci/utils/xml/salesforce_encoding.py +100 -0
- cumulusci/utils/xml/test/test_metadata_tree.py +251 -0
- cumulusci/utils/xml/test/test_salesforce_encoding.py +173 -0
- cumulusci/utils/yaml/cumulusci_yml.py +401 -0
- cumulusci/utils/yaml/model_parser.py +156 -0
- cumulusci/utils/yaml/safer_loader.py +74 -0
- cumulusci/utils/yaml/tests/bad_cci.yml +5 -0
- cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_errors.yaml +20 -0
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +286 -0
- cumulusci/utils/yaml/tests/test_model_parser.py +175 -0
- cumulusci/utils/yaml/tests/test_safer_loader.py +88 -0
- cumulusci/utils/ziputils.py +61 -0
- cumulusci/vcs/base.py +143 -0
- cumulusci/vcs/bootstrap.py +272 -0
- cumulusci/vcs/github/__init__.py +24 -0
- cumulusci/vcs/github/adapter.py +689 -0
- cumulusci/vcs/github/release_notes/generator.py +219 -0
- cumulusci/vcs/github/release_notes/parser.py +151 -0
- cumulusci/vcs/github/release_notes/provider.py +143 -0
- cumulusci/vcs/github/service.py +569 -0
- cumulusci/vcs/github/tests/test_adapter.py +138 -0
- cumulusci/vcs/github/tests/test_service.py +408 -0
- cumulusci/vcs/models.py +586 -0
- cumulusci/vcs/tests/conftest.py +41 -0
- cumulusci/vcs/tests/dummy_service.py +241 -0
- cumulusci/vcs/tests/test_vcs_base.py +687 -0
- cumulusci/vcs/tests/test_vcs_bootstrap.py +727 -0
- cumulusci/vcs/utils/__init__.py +31 -0
- cumulusci/vcs/vcs_source.py +287 -0
- cumulusci_plus-5.0.0.dist-info/METADATA +145 -0
- cumulusci_plus-5.0.0.dist-info/RECORD +744 -0
- cumulusci_plus-5.0.0.dist-info/WHEEL +4 -0
- cumulusci_plus-5.0.0.dist-info/entry_points.txt +3 -0
- cumulusci_plus-5.0.0.dist-info/licenses/AUTHORS.rst +41 -0
- cumulusci_plus-5.0.0.dist-info/licenses/LICENSE +30 -0
|
@@ -0,0 +1,3175 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import shutil
|
|
7
|
+
import string
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections import namedtuple
|
|
10
|
+
from contextlib import nullcontext
|
|
11
|
+
from datetime import date, timedelta
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest import mock
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
import responses
|
|
17
|
+
from sqlalchemy import Column, Table, Unicode, create_engine
|
|
18
|
+
|
|
19
|
+
from cumulusci.core.exceptions import BulkDataException, TaskOptionsError
|
|
20
|
+
from cumulusci.salesforce_api.org_schema import get_org_schema
|
|
21
|
+
from cumulusci.tasks.bulkdata import LoadData
|
|
22
|
+
from cumulusci.tasks.bulkdata.load import (
|
|
23
|
+
CreateRollback,
|
|
24
|
+
Rollback,
|
|
25
|
+
RollbackType,
|
|
26
|
+
UpdateRollback,
|
|
27
|
+
)
|
|
28
|
+
from cumulusci.tasks.bulkdata.mapping_parser import MappingLookup, MappingStep
|
|
29
|
+
from cumulusci.tasks.bulkdata.step import (
|
|
30
|
+
BulkApiDmlOperation,
|
|
31
|
+
DataApi,
|
|
32
|
+
DataOperationJobResult,
|
|
33
|
+
DataOperationResult,
|
|
34
|
+
DataOperationStatus,
|
|
35
|
+
DataOperationType,
|
|
36
|
+
)
|
|
37
|
+
from cumulusci.tasks.bulkdata.tests.utils import (
|
|
38
|
+
FakeBulkAPI,
|
|
39
|
+
FakeBulkAPIDmlOperation,
|
|
40
|
+
_make_task,
|
|
41
|
+
)
|
|
42
|
+
from cumulusci.tests.util import (
|
|
43
|
+
CURRENT_SF_API_VERSION,
|
|
44
|
+
DummyOrgConfig,
|
|
45
|
+
assert_max_memory_usage,
|
|
46
|
+
mock_describe_calls,
|
|
47
|
+
)
|
|
48
|
+
from cumulusci.utils import temporary_dir
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class FakePath:
|
|
52
|
+
def __init__(self, real_path, does_exist):
|
|
53
|
+
self.does_exist = does_exist
|
|
54
|
+
self.real_path = real_path
|
|
55
|
+
|
|
56
|
+
def exists(self):
|
|
57
|
+
return self.does_exist
|
|
58
|
+
|
|
59
|
+
def __str__(self):
|
|
60
|
+
return self.real_path
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FakePathFileSystem:
|
|
64
|
+
def __init__(self, existing_paths):
|
|
65
|
+
self.paths = [Path(path) for path in existing_paths]
|
|
66
|
+
|
|
67
|
+
def __call__(self, *filename):
|
|
68
|
+
filename_parts = [str(el) for el in filename]
|
|
69
|
+
real_filename = str(Path(*filename_parts))
|
|
70
|
+
return FakePath(real_filename, (Path(real_filename) in self.paths))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TestLoadData:
|
|
74
|
+
mapping_file = "mapping_v1.yml"
|
|
75
|
+
|
|
76
|
+
@responses.activate
|
|
77
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
78
|
+
def test_run(self, dml_mock):
|
|
79
|
+
responses.add(
|
|
80
|
+
method="GET",
|
|
81
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id+FROM+RecordType+WHERE+SObjectType%3D%27Account%27AND+DeveloperName+%3D+%27HH_Account%27+LIMIT+1",
|
|
82
|
+
body=json.dumps({"records": [{"Id": "1"}]}),
|
|
83
|
+
status=200,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
base_path = os.path.dirname(__file__)
|
|
87
|
+
db_path = os.path.join(base_path, "testdata.db")
|
|
88
|
+
mapping_path = os.path.join(base_path, self.mapping_file)
|
|
89
|
+
|
|
90
|
+
with temporary_dir() as d:
|
|
91
|
+
tmp_db_path = os.path.join(d, "testdata.db")
|
|
92
|
+
shutil.copyfile(db_path, tmp_db_path)
|
|
93
|
+
|
|
94
|
+
task = _make_task(
|
|
95
|
+
LoadData,
|
|
96
|
+
{
|
|
97
|
+
"options": {
|
|
98
|
+
"database_url": f"sqlite:///{tmp_db_path}",
|
|
99
|
+
"mapping": mapping_path,
|
|
100
|
+
"set_recently_viewed": False,
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
task.bulk = mock.Mock()
|
|
106
|
+
task.sf = mock.Mock()
|
|
107
|
+
|
|
108
|
+
step = FakeBulkAPIDmlOperation(
|
|
109
|
+
sobject="Contact",
|
|
110
|
+
operation=DataOperationType.INSERT,
|
|
111
|
+
api_options={},
|
|
112
|
+
context=task,
|
|
113
|
+
fields=[],
|
|
114
|
+
)
|
|
115
|
+
dml_mock.return_value = step
|
|
116
|
+
|
|
117
|
+
step.results = [
|
|
118
|
+
DataOperationResult("001000000000000", True, None),
|
|
119
|
+
DataOperationResult("003000000000000", True, None),
|
|
120
|
+
DataOperationResult("003000000000001", True, None),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
mock_describe_calls()
|
|
124
|
+
task()
|
|
125
|
+
assert step.records == [
|
|
126
|
+
["TestHousehold", "TestHousehold", "1"],
|
|
127
|
+
["Test", "User", "test@example.com", "001000000000000"],
|
|
128
|
+
["Error", "User", "error@example.com", "001000000000000"],
|
|
129
|
+
]
|
|
130
|
+
with create_engine(task.options["database_url"]).connect() as c:
|
|
131
|
+
hh_ids = next(c.execute("SELECT * from cumulusci_id_table"))
|
|
132
|
+
assert hh_ids == ("households-1", "001000000000000")
|
|
133
|
+
|
|
134
|
+
@responses.activate
|
|
135
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
136
|
+
def test__insert_rollback(self, dml_mock):
|
|
137
|
+
task = _make_task(
|
|
138
|
+
LoadData,
|
|
139
|
+
{
|
|
140
|
+
"options": {
|
|
141
|
+
"database_url": "sqlite://",
|
|
142
|
+
"mapping": "mapping.yml",
|
|
143
|
+
"start_step": "Insert Contacts",
|
|
144
|
+
"set_recently_viewed": False,
|
|
145
|
+
"enable_rollback": True,
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
)
|
|
149
|
+
table = mock.Mock()
|
|
150
|
+
p = mock.PropertyMock(return_value=f"Contact_{RollbackType.INSERT}")
|
|
151
|
+
type(table).name = p
|
|
152
|
+
task._initialized_rollback_tables_api = {
|
|
153
|
+
f"Contact_{RollbackType.INSERT}": "rest"
|
|
154
|
+
}
|
|
155
|
+
task.metadata = mock.Mock(sorted_tables=[table])
|
|
156
|
+
task.session = mock.Mock(
|
|
157
|
+
query=mock.Mock(
|
|
158
|
+
return_value=mock.Mock(
|
|
159
|
+
all=mock.Mock(return_value=[mock.Mock(sf_id="00001111")]),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
Rollback._initialized_rollback_tables_api = {"Contact_insert_rollback": "rest"}
|
|
165
|
+
CreateRollback._perform_rollback(task, table)
|
|
166
|
+
|
|
167
|
+
dml_mock.assert_called_once_with(
|
|
168
|
+
sobject="Contact",
|
|
169
|
+
operation=(DataOperationType.DELETE),
|
|
170
|
+
fields=["Id"],
|
|
171
|
+
api_options={},
|
|
172
|
+
context=task,
|
|
173
|
+
api="rest",
|
|
174
|
+
volume=1,
|
|
175
|
+
)
|
|
176
|
+
dml_mock.return_value.start.assert_called_once()
|
|
177
|
+
dml_mock.return_value.end.assert_called_once()
|
|
178
|
+
|
|
179
|
+
@responses.activate
|
|
180
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
181
|
+
def test__upsert_rollback(self, dml_mock):
|
|
182
|
+
task = _make_task(
|
|
183
|
+
LoadData,
|
|
184
|
+
{
|
|
185
|
+
"options": {
|
|
186
|
+
"database_url": "sqlite://",
|
|
187
|
+
"mapping": "mapping.yml",
|
|
188
|
+
"start_step": "Upsert Contacts",
|
|
189
|
+
"set_recently_viewed": False,
|
|
190
|
+
"enable_rollback": True,
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
table = mock.Mock()
|
|
195
|
+
p = mock.PropertyMock(return_value=f"Contact_{RollbackType.UPSERT}")
|
|
196
|
+
type(table).name = p
|
|
197
|
+
Column = namedtuple("Column", ["name"])
|
|
198
|
+
type(table).columns = [Column("Id"), Column("LastName")]
|
|
199
|
+
task._initialized_rollback_tables_api = {
|
|
200
|
+
f"Contact_{RollbackType.UPSERT}": "rest"
|
|
201
|
+
}
|
|
202
|
+
task.metadata = mock.Mock(sorted_tables=[table])
|
|
203
|
+
task.session = mock.Mock(
|
|
204
|
+
query=mock.Mock(
|
|
205
|
+
return_value=mock.Mock(
|
|
206
|
+
all=mock.Mock(
|
|
207
|
+
return_value=[mock.Mock(Id="00001111", LastName="TestName")]
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
Rollback._initialized_rollback_tables_api = {"Contact_upsert_rollback": "rest"}
|
|
214
|
+
UpdateRollback._perform_rollback(task, table)
|
|
215
|
+
|
|
216
|
+
dml_mock.assert_called_once_with(
|
|
217
|
+
sobject="Contact",
|
|
218
|
+
operation=(DataOperationType.UPSERT),
|
|
219
|
+
fields=["Id", "LastName"],
|
|
220
|
+
api_options={"update_key": "Id"},
|
|
221
|
+
context=task,
|
|
222
|
+
api="rest",
|
|
223
|
+
volume=1,
|
|
224
|
+
)
|
|
225
|
+
dml_mock.return_value.start.assert_called_once()
|
|
226
|
+
dml_mock.return_value.end.assert_called_once()
|
|
227
|
+
|
|
228
|
+
def test__perform_rollback(self):
|
|
229
|
+
task = _make_task(
|
|
230
|
+
LoadData,
|
|
231
|
+
{
|
|
232
|
+
"options": {
|
|
233
|
+
"database_url": "sqlite://",
|
|
234
|
+
"mapping": "mapping.yml",
|
|
235
|
+
"start_step": "Insert Contacts",
|
|
236
|
+
"set_recently_viewed": False,
|
|
237
|
+
"enable_rollback": True,
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
)
|
|
241
|
+
table_insert = mock.Mock()
|
|
242
|
+
p = mock.PropertyMock(return_value=f"Contact_{RollbackType.INSERT}")
|
|
243
|
+
type(table_insert).name = p
|
|
244
|
+
table_upsert = mock.Mock()
|
|
245
|
+
p = mock.PropertyMock(return_value=f"Account_{RollbackType.UPSERT}")
|
|
246
|
+
type(table_upsert).name = p
|
|
247
|
+
task.metadata = mock.Mock()
|
|
248
|
+
task.metadata.sorted_tables = [table_insert, table_upsert]
|
|
249
|
+
|
|
250
|
+
with mock.patch.object(
|
|
251
|
+
CreateRollback, "_perform_rollback"
|
|
252
|
+
) as mock_insert_rollback, mock.patch.object(
|
|
253
|
+
UpdateRollback, "_perform_rollback"
|
|
254
|
+
) as mock_upsert_rollback:
|
|
255
|
+
Rollback._perform_rollback(task)
|
|
256
|
+
|
|
257
|
+
mock_insert_rollback.assert_called_once_with(task, table_insert)
|
|
258
|
+
mock_upsert_rollback.assert_called_once_with(task, table_upsert)
|
|
259
|
+
|
|
260
|
+
def test_run_task__start_step(self):
|
|
261
|
+
task = _make_task(
|
|
262
|
+
LoadData,
|
|
263
|
+
{
|
|
264
|
+
"options": {
|
|
265
|
+
"database_url": "sqlite://",
|
|
266
|
+
"mapping": "mapping.yml",
|
|
267
|
+
"start_step": "Insert Contacts",
|
|
268
|
+
"set_recently_viewed": False,
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
)
|
|
272
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
273
|
+
task._initialize_id_table = mock.Mock()
|
|
274
|
+
task._init_mapping = mock.Mock()
|
|
275
|
+
task.mapping = {}
|
|
276
|
+
task.mapping["Insert Households"] = MappingStep(sf_object="one", fields={})
|
|
277
|
+
task.mapping["Insert Contacts"] = MappingStep(sf_object="two", fields={})
|
|
278
|
+
task.after_steps = {}
|
|
279
|
+
task._execute_step = mock.Mock(
|
|
280
|
+
return_value=DataOperationJobResult(DataOperationStatus.SUCCESS, [], 0, 0)
|
|
281
|
+
)
|
|
282
|
+
task()
|
|
283
|
+
task._execute_step.assert_called_once_with(
|
|
284
|
+
MappingStep(sf_object="two", fields={})
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def test_run_task__after_steps(self):
|
|
288
|
+
task = _make_task(
|
|
289
|
+
LoadData,
|
|
290
|
+
{
|
|
291
|
+
"options": {
|
|
292
|
+
"database_url": "sqlite://",
|
|
293
|
+
"mapping": "mapping.yml",
|
|
294
|
+
"set_recently_viewed": False,
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
)
|
|
298
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
299
|
+
task._init_mapping = mock.Mock()
|
|
300
|
+
task._expand_mapping = mock.Mock()
|
|
301
|
+
task._initialize_id_table = mock.Mock()
|
|
302
|
+
task.mapping = {}
|
|
303
|
+
one = task.mapping["Insert Households"] = mock.Mock()
|
|
304
|
+
two = task.mapping["Insert Contacts"] = mock.Mock()
|
|
305
|
+
households_steps = {}
|
|
306
|
+
households_steps["four"] = 4
|
|
307
|
+
households_steps["five"] = 5
|
|
308
|
+
task.after_steps = {
|
|
309
|
+
"Insert Contacts": {"three": 3},
|
|
310
|
+
"Insert Households": households_steps,
|
|
311
|
+
}
|
|
312
|
+
task._execute_step = mock.Mock(
|
|
313
|
+
return_value=DataOperationJobResult(DataOperationStatus.SUCCESS, [], 0, 0)
|
|
314
|
+
)
|
|
315
|
+
task()
|
|
316
|
+
task._execute_step.assert_has_calls(
|
|
317
|
+
[mock.call(one), mock.call(4), mock.call(5), mock.call(two), mock.call(3)]
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
def test_run_task__after_steps_failure(self):
|
|
321
|
+
task = _make_task(
|
|
322
|
+
LoadData,
|
|
323
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
324
|
+
)
|
|
325
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
326
|
+
task._init_mapping = mock.Mock()
|
|
327
|
+
task._expand_mapping = mock.Mock()
|
|
328
|
+
task._initialize_id_table = mock.Mock()
|
|
329
|
+
task.mapping = {}
|
|
330
|
+
task.mapping["Insert Households"] = 1
|
|
331
|
+
task.mapping["Insert Contacts"] = 2
|
|
332
|
+
households_steps = {}
|
|
333
|
+
households_steps["four"] = 4
|
|
334
|
+
households_steps["five"] = 5
|
|
335
|
+
task.after_steps = {
|
|
336
|
+
"Insert Contacts": {"three": 3},
|
|
337
|
+
"Insert Households": households_steps,
|
|
338
|
+
}
|
|
339
|
+
task._execute_step = mock.Mock(
|
|
340
|
+
side_effect=[
|
|
341
|
+
DataOperationJobResult(DataOperationStatus.SUCCESS, [], 0, 0),
|
|
342
|
+
DataOperationJobResult(DataOperationStatus.JOB_FAILURE, [], 0, 0),
|
|
343
|
+
]
|
|
344
|
+
)
|
|
345
|
+
with pytest.raises(BulkDataException):
|
|
346
|
+
task()
|
|
347
|
+
|
|
348
|
+
@responses.activate
|
|
349
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
350
|
+
def test_run__sql(self, dml_mock):
|
|
351
|
+
responses.add(
|
|
352
|
+
method="GET",
|
|
353
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id+FROM+RecordType+WHERE+SObjectType%3D%27Account%27AND+DeveloperName+%3D+%27HH_Account%27+LIMIT+1",
|
|
354
|
+
body=json.dumps({"records": [{"Id": "1"}]}),
|
|
355
|
+
status=200,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
base_path = os.path.dirname(__file__)
|
|
359
|
+
sql_path = os.path.join(base_path, "testdata.sql")
|
|
360
|
+
mapping_path = os.path.join(base_path, self.mapping_file)
|
|
361
|
+
|
|
362
|
+
task = _make_task(
|
|
363
|
+
LoadData,
|
|
364
|
+
{
|
|
365
|
+
"options": {
|
|
366
|
+
"sql_path": sql_path,
|
|
367
|
+
"mapping": mapping_path,
|
|
368
|
+
"set_recently_viewed": False,
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
)
|
|
372
|
+
task.bulk = mock.Mock()
|
|
373
|
+
task.sf = mock.Mock()
|
|
374
|
+
step = FakeBulkAPIDmlOperation(
|
|
375
|
+
sobject="Contact",
|
|
376
|
+
operation=DataOperationType.INSERT,
|
|
377
|
+
api_options={},
|
|
378
|
+
context=task,
|
|
379
|
+
fields=[],
|
|
380
|
+
)
|
|
381
|
+
dml_mock.return_value = step
|
|
382
|
+
step.results = [
|
|
383
|
+
DataOperationResult("001000000000000", True, None),
|
|
384
|
+
DataOperationResult("003000000000000", True, None),
|
|
385
|
+
DataOperationResult("003000000000001", True, None),
|
|
386
|
+
]
|
|
387
|
+
mock_describe_calls()
|
|
388
|
+
task()
|
|
389
|
+
assert step.records == [
|
|
390
|
+
[None, "TestHousehold", "1"],
|
|
391
|
+
["Test☃", "User", "test@example.com", "001000000000000"],
|
|
392
|
+
["Error", "User", "error@example.com", "001000000000000"],
|
|
393
|
+
]
|
|
394
|
+
|
|
395
|
+
def test_init_options__missing_input(self):
|
|
396
|
+
t = _make_task(LoadData, {"options": {}})
|
|
397
|
+
|
|
398
|
+
assert t.options["sql_path"] == "datasets/sample.sql"
|
|
399
|
+
assert t.options["mapping"] == "datasets/mapping.yml"
|
|
400
|
+
|
|
401
|
+
def test_init_options__bulk_mode(self):
|
|
402
|
+
t = _make_task(
|
|
403
|
+
LoadData,
|
|
404
|
+
{
|
|
405
|
+
"options": {
|
|
406
|
+
"database_url": "file:///test.db",
|
|
407
|
+
"mapping": "mapping.yml",
|
|
408
|
+
"bulk_mode": "Serial",
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
assert t.bulk_mode == "Serial"
|
|
414
|
+
|
|
415
|
+
t = _make_task(
|
|
416
|
+
LoadData,
|
|
417
|
+
{"options": {"database_url": "file:///test.db", "mapping": "mapping.yml"}},
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
assert t.bulk_mode is None
|
|
421
|
+
|
|
422
|
+
def test_init_options__bulk_mode_wrong(self):
|
|
423
|
+
with pytest.raises(TaskOptionsError):
|
|
424
|
+
_make_task(LoadData, {"options": {"bulk_mode": "Test"}})
|
|
425
|
+
|
|
426
|
+
def test_init_options__database_url(self):
|
|
427
|
+
t = _make_task(
|
|
428
|
+
LoadData,
|
|
429
|
+
{"options": {"database_url": "file:///test.db", "mapping": "mapping.yml"}},
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
assert t.options["database_url"] == "file:///test.db"
|
|
433
|
+
assert t.options["sql_path"] is None
|
|
434
|
+
assert t.has_dataset is True
|
|
435
|
+
|
|
436
|
+
def test_init_options__sql_path_and_mapping(self):
|
|
437
|
+
t = _make_task(
|
|
438
|
+
LoadData,
|
|
439
|
+
{
|
|
440
|
+
"options": {
|
|
441
|
+
"sql_path": "datasets/test.sql",
|
|
442
|
+
"mapping": "datasets/test.yml",
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
assert t.options["sql_path"] == "datasets/test.sql"
|
|
448
|
+
assert t.options["mapping"] == "datasets/test.yml"
|
|
449
|
+
assert t.options["database_url"] is None
|
|
450
|
+
assert t.has_dataset is True
|
|
451
|
+
|
|
452
|
+
def test_init_options__org_shape_match_only__true(self):
|
|
453
|
+
dataset_path = "datasets/dev/dev.dataset.sql"
|
|
454
|
+
mapping_path = "datasets/dev/dev.mapping.yml"
|
|
455
|
+
fake_paths = FakePathFileSystem([mapping_path, dataset_path, "datasets/dev"])
|
|
456
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
457
|
+
org_config = DummyOrgConfig(
|
|
458
|
+
{
|
|
459
|
+
"config_name": "dev",
|
|
460
|
+
},
|
|
461
|
+
"test",
|
|
462
|
+
)
|
|
463
|
+
t = _make_task(
|
|
464
|
+
LoadData,
|
|
465
|
+
{
|
|
466
|
+
"options": {
|
|
467
|
+
"sql_path": "datasets/test.sql",
|
|
468
|
+
"mapping": "datasets/mapping.yml",
|
|
469
|
+
"org_shape_match_only": True,
|
|
470
|
+
}
|
|
471
|
+
},
|
|
472
|
+
org_config,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
assert t.options["sql_path"] == dataset_path
|
|
476
|
+
assert t.options["mapping"] == mapping_path
|
|
477
|
+
assert t.options["database_url"] is None
|
|
478
|
+
assert t.has_dataset is True
|
|
479
|
+
|
|
480
|
+
def test_init_options__org_shape_match_only__false(self):
|
|
481
|
+
"""Matching paths exist but we do not use them."""
|
|
482
|
+
dataset_path = "datasets/dev/dev.dataset.sql"
|
|
483
|
+
mapping_path = "datasets/dev/dev.mapping.yml"
|
|
484
|
+
fake_paths = FakePathFileSystem([mapping_path, dataset_path, "datasets/dev"])
|
|
485
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
486
|
+
org_config = DummyOrgConfig(
|
|
487
|
+
{
|
|
488
|
+
"config_name": "dev",
|
|
489
|
+
},
|
|
490
|
+
"test",
|
|
491
|
+
)
|
|
492
|
+
t = _make_task(
|
|
493
|
+
LoadData,
|
|
494
|
+
{
|
|
495
|
+
"options": {
|
|
496
|
+
"sql_path": "datasets/test.sql",
|
|
497
|
+
"mapping": "datasets/mapping.yml",
|
|
498
|
+
# "org_shape_match_only": False, DEFAULTED
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
org_config,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
assert t.options["sql_path"] == "datasets/test.sql"
|
|
505
|
+
assert t.options["mapping"] == "datasets/mapping.yml"
|
|
506
|
+
assert t.options["database_url"] is None
|
|
507
|
+
assert t.has_dataset is True
|
|
508
|
+
|
|
509
|
+
def test_init_options__sql_path_no_mapping(self):
|
|
510
|
+
t = _make_task(LoadData, {"options": {"sql_path": "datasets/test.sql"}})
|
|
511
|
+
|
|
512
|
+
assert t.options["sql_path"] == "datasets/test.sql"
|
|
513
|
+
assert t.options["mapping"] == "datasets/mapping.yml"
|
|
514
|
+
assert t.options["database_url"] is None
|
|
515
|
+
assert t.has_dataset is True
|
|
516
|
+
|
|
517
|
+
def test_init_options__mapping_no_sql_path(self):
|
|
518
|
+
t = _make_task(LoadData, {"options": {"mapping": "datasets/test.yml"}})
|
|
519
|
+
|
|
520
|
+
assert t.options["sql_path"] == "datasets/sample.sql"
|
|
521
|
+
assert t.options["mapping"] == "datasets/test.yml"
|
|
522
|
+
assert t.options["database_url"] is None
|
|
523
|
+
assert t.has_dataset is True
|
|
524
|
+
|
|
525
|
+
def test_init_datasets__matching_dataset(self):
|
|
526
|
+
dataset_path = "datasets/dev/dev.dataset.sql"
|
|
527
|
+
mapping_path = "datasets/dev/dev.mapping.yml"
|
|
528
|
+
fake_paths = FakePathFileSystem([mapping_path, dataset_path, "datasets/dev"])
|
|
529
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
530
|
+
org_config = DummyOrgConfig(
|
|
531
|
+
{
|
|
532
|
+
"config_name": "dev",
|
|
533
|
+
},
|
|
534
|
+
"test",
|
|
535
|
+
)
|
|
536
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
537
|
+
assert t.options["sql_path"] == dataset_path
|
|
538
|
+
assert t.options["mapping"] == mapping_path
|
|
539
|
+
assert t.has_dataset is True
|
|
540
|
+
|
|
541
|
+
def test_init_datasets__matching_dataset_dir__missing_dataset_path(self, caplog):
|
|
542
|
+
caplog.set_level(logging.WARNING)
|
|
543
|
+
mapping_path = "datasets/dev/dev.mapping.yml"
|
|
544
|
+
fake_paths = FakePathFileSystem([mapping_path, "datasets/dev"])
|
|
545
|
+
org_shape = "dev"
|
|
546
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
547
|
+
org_config = DummyOrgConfig(
|
|
548
|
+
{
|
|
549
|
+
"config_name": org_shape,
|
|
550
|
+
},
|
|
551
|
+
"test",
|
|
552
|
+
)
|
|
553
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
554
|
+
assert t.options.get("sql_path") is None
|
|
555
|
+
assert t.options.get("mapping") is None
|
|
556
|
+
assert (
|
|
557
|
+
f"Found datasets/{org_shape} but it did not contain {org_shape}.mapping.yml and {org_shape}.dataset.yml."
|
|
558
|
+
in caplog.text
|
|
559
|
+
)
|
|
560
|
+
assert t.has_dataset is False
|
|
561
|
+
|
|
562
|
+
def test_init_datasets__matching_dataset_dir__missing_mapping_path(self, caplog):
|
|
563
|
+
caplog.set_level(logging.WARNING)
|
|
564
|
+
dataset_path = "datasets/dev/dev.dataset.sql"
|
|
565
|
+
fake_paths = FakePathFileSystem([dataset_path, "datasets/dev"])
|
|
566
|
+
org_shape = "dev"
|
|
567
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
568
|
+
org_config = DummyOrgConfig(
|
|
569
|
+
{
|
|
570
|
+
"config_name": org_shape,
|
|
571
|
+
},
|
|
572
|
+
"test",
|
|
573
|
+
)
|
|
574
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
575
|
+
assert t.options.get("sql_path") is None
|
|
576
|
+
assert t.options.get("mapping") is None
|
|
577
|
+
assert (
|
|
578
|
+
f"Found datasets/{org_shape} but it did not contain {org_shape}.mapping.yml and {org_shape}.dataset.yml."
|
|
579
|
+
in caplog.text
|
|
580
|
+
)
|
|
581
|
+
assert t.has_dataset is False
|
|
582
|
+
|
|
583
|
+
def test_init_datasets__no_matching_dataset__use_default(self):
|
|
584
|
+
dataset_path = "datasets/sample.sql"
|
|
585
|
+
mapping_path = "datasets/mapping.yml"
|
|
586
|
+
fake_paths = FakePathFileSystem([dataset_path, mapping_path])
|
|
587
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
588
|
+
org_config = DummyOrgConfig(
|
|
589
|
+
{
|
|
590
|
+
"config_name": "dev",
|
|
591
|
+
},
|
|
592
|
+
"test",
|
|
593
|
+
)
|
|
594
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
595
|
+
assert t.options["sql_path"] == "datasets/sample.sql"
|
|
596
|
+
assert t.options["mapping"] == "datasets/mapping.yml"
|
|
597
|
+
assert t.has_dataset is True
|
|
598
|
+
|
|
599
|
+
def test_init_datasets__no_matching_dataset__skip_default(self):
|
|
600
|
+
dataset_path = "datasets/sample.sql"
|
|
601
|
+
mapping_path = "datasets/mapping.yml"
|
|
602
|
+
fake_paths = FakePathFileSystem([dataset_path, mapping_path])
|
|
603
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
604
|
+
org_config = DummyOrgConfig(
|
|
605
|
+
{
|
|
606
|
+
"config_name": "dev",
|
|
607
|
+
},
|
|
608
|
+
"test",
|
|
609
|
+
)
|
|
610
|
+
t = _make_task(
|
|
611
|
+
LoadData,
|
|
612
|
+
{
|
|
613
|
+
"options": {
|
|
614
|
+
"org_shape_match_only": True,
|
|
615
|
+
}
|
|
616
|
+
},
|
|
617
|
+
org_config=org_config,
|
|
618
|
+
)
|
|
619
|
+
assert t.options["sql_path"] is None
|
|
620
|
+
assert t.options["mapping"] is None
|
|
621
|
+
assert t.has_dataset is False
|
|
622
|
+
|
|
623
|
+
def test_init_datasets__no_matching_dataset_no_load_scratch(self, caplog):
|
|
624
|
+
caplog.set_level(logging.INFO)
|
|
625
|
+
fake_paths = FakePathFileSystem([])
|
|
626
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
627
|
+
org_config = DummyOrgConfig(
|
|
628
|
+
{
|
|
629
|
+
"config_name": "dev",
|
|
630
|
+
},
|
|
631
|
+
"test",
|
|
632
|
+
)
|
|
633
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
634
|
+
assert t.has_dataset is False
|
|
635
|
+
assert t.options.get("sql_path") is None
|
|
636
|
+
assert t.options.get("mapping") is None
|
|
637
|
+
t._run_task()
|
|
638
|
+
assert (
|
|
639
|
+
"No data will be loaded because there was no dataset found matching your org shape name ('dev')."
|
|
640
|
+
in caplog.text
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
def test_init_datasets__no_matching_dataset_no_load_persistent(self, caplog):
|
|
644
|
+
caplog.set_level(logging.INFO)
|
|
645
|
+
fake_paths = FakePathFileSystem([])
|
|
646
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.Path", fake_paths):
|
|
647
|
+
org_config = DummyOrgConfig(
|
|
648
|
+
{
|
|
649
|
+
"config_name": None,
|
|
650
|
+
},
|
|
651
|
+
"test",
|
|
652
|
+
)
|
|
653
|
+
t = _make_task(LoadData, {}, org_config=org_config)
|
|
654
|
+
assert t.has_dataset is False
|
|
655
|
+
assert t.options.get("sql_path") is None
|
|
656
|
+
assert t.options.get("mapping") is None
|
|
657
|
+
t._run_task()
|
|
658
|
+
assert (
|
|
659
|
+
"No data will be loaded because this is a persistent org and no dataset was specified."
|
|
660
|
+
in caplog.text
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping")
|
|
664
|
+
def test_init_mapping_passes_options_to_validate(self, validate_and_inject_mapping):
|
|
665
|
+
base_path = os.path.dirname(__file__)
|
|
666
|
+
|
|
667
|
+
t = _make_task(
|
|
668
|
+
LoadData,
|
|
669
|
+
{
|
|
670
|
+
"options": {
|
|
671
|
+
"sql_path": "test.sql",
|
|
672
|
+
"mapping": os.path.join(base_path, self.mapping_file),
|
|
673
|
+
"inject_namespaces": True,
|
|
674
|
+
"drop_missing_schema": True,
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
t._init_task()
|
|
680
|
+
t._init_mapping()
|
|
681
|
+
|
|
682
|
+
validate_and_inject_mapping.assert_called_once_with(
|
|
683
|
+
mapping=t.mapping,
|
|
684
|
+
sf=t.sf,
|
|
685
|
+
namespace=t.project_config.project__package__namespace,
|
|
686
|
+
data_operation=DataOperationType.INSERT,
|
|
687
|
+
inject_namespaces=True,
|
|
688
|
+
drop_missing=True,
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
@responses.activate
|
|
692
|
+
def test_expand_mapping_creates_after_steps(self):
|
|
693
|
+
base_path = os.path.dirname(__file__)
|
|
694
|
+
mapping_path = os.path.join(base_path, "mapping_after.yml")
|
|
695
|
+
task = _make_task(
|
|
696
|
+
LoadData,
|
|
697
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
mock_describe_calls()
|
|
701
|
+
task._init_task()
|
|
702
|
+
task._init_mapping()
|
|
703
|
+
|
|
704
|
+
model = mock.Mock()
|
|
705
|
+
model.__table__ = mock.Mock()
|
|
706
|
+
model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
707
|
+
task.models = {"accounts": model, "contacts": model}
|
|
708
|
+
|
|
709
|
+
task._expand_mapping()
|
|
710
|
+
|
|
711
|
+
assert {} == task.after_steps["Insert Opportunities"]
|
|
712
|
+
assert [
|
|
713
|
+
"Update Account Dependencies After Insert Contacts",
|
|
714
|
+
"Update Contact Dependencies After Insert Contacts",
|
|
715
|
+
] == list(task.after_steps["Insert Contacts"].keys())
|
|
716
|
+
lookups = {}
|
|
717
|
+
lookups["Id"] = MappingLookup(name="Id", table="accounts", key_field="sf_id")
|
|
718
|
+
lookups["Primary_Contact__c"] = MappingLookup(
|
|
719
|
+
table="contacts", name="Primary_Contact__c"
|
|
720
|
+
)
|
|
721
|
+
assert (
|
|
722
|
+
MappingStep(
|
|
723
|
+
sf_object="Account",
|
|
724
|
+
api=DataApi.BULK,
|
|
725
|
+
action=DataOperationType.UPDATE,
|
|
726
|
+
table="accounts",
|
|
727
|
+
lookups=lookups,
|
|
728
|
+
fields={},
|
|
729
|
+
)
|
|
730
|
+
== task.after_steps["Insert Contacts"][
|
|
731
|
+
"Update Account Dependencies After Insert Contacts"
|
|
732
|
+
]
|
|
733
|
+
)
|
|
734
|
+
lookups = {}
|
|
735
|
+
lookups["Id"] = MappingLookup(name="Id", table="contacts", key_field="sf_id")
|
|
736
|
+
lookups["ReportsToId"] = MappingLookup(table="contacts", name="ReportsToId")
|
|
737
|
+
assert (
|
|
738
|
+
MappingStep(
|
|
739
|
+
sf_object="Contact",
|
|
740
|
+
api=DataApi.BULK,
|
|
741
|
+
action=DataOperationType.UPDATE,
|
|
742
|
+
table="contacts",
|
|
743
|
+
fields={},
|
|
744
|
+
lookups=lookups,
|
|
745
|
+
)
|
|
746
|
+
== task.after_steps["Insert Contacts"][
|
|
747
|
+
"Update Contact Dependencies After Insert Contacts"
|
|
748
|
+
]
|
|
749
|
+
)
|
|
750
|
+
assert ["Update Account Dependencies After Insert Accounts"] == list(
|
|
751
|
+
task.after_steps["Insert Accounts"].keys()
|
|
752
|
+
)
|
|
753
|
+
lookups = {}
|
|
754
|
+
lookups["Id"] = MappingLookup(name="Id", table="accounts", key_field="sf_id")
|
|
755
|
+
lookups["ParentId"] = MappingLookup(table="accounts", name="ParentId")
|
|
756
|
+
assert (
|
|
757
|
+
MappingStep(
|
|
758
|
+
sf_object="Account",
|
|
759
|
+
api=DataApi.BULK,
|
|
760
|
+
action=DataOperationType.UPDATE,
|
|
761
|
+
table="accounts",
|
|
762
|
+
fields={},
|
|
763
|
+
lookups=lookups,
|
|
764
|
+
)
|
|
765
|
+
== task.after_steps["Insert Accounts"][
|
|
766
|
+
"Update Account Dependencies After Insert Accounts"
|
|
767
|
+
]
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
def test_stream_queried_data__skips_empty_rows(self):
|
|
771
|
+
task = _make_task(
|
|
772
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
773
|
+
)
|
|
774
|
+
task.sf = mock.Mock()
|
|
775
|
+
|
|
776
|
+
mapping = MappingStep(
|
|
777
|
+
**{
|
|
778
|
+
"sf_object": "Account",
|
|
779
|
+
"action": "update",
|
|
780
|
+
"fields": {},
|
|
781
|
+
"lookups": {
|
|
782
|
+
"Id": MappingLookup(
|
|
783
|
+
**{"table": "accounts", "key_field": "account_id"}
|
|
784
|
+
),
|
|
785
|
+
"ParentId": MappingLookup(**{"table": "accounts"}),
|
|
786
|
+
},
|
|
787
|
+
}
|
|
788
|
+
)
|
|
789
|
+
|
|
790
|
+
task._query_db = mock.Mock()
|
|
791
|
+
task._query_db.return_value.yield_per = mock.Mock(
|
|
792
|
+
return_value=[
|
|
793
|
+
# Local Id, Loaded Id, Parent Id
|
|
794
|
+
["001000000001", "001000000005", "001000000007"],
|
|
795
|
+
["001000000002", "001000000006", "001000000008"],
|
|
796
|
+
["001000000003", "001000000009", None],
|
|
797
|
+
]
|
|
798
|
+
)
|
|
799
|
+
|
|
800
|
+
with tempfile.TemporaryFile("w+t") as local_ids:
|
|
801
|
+
records = list(
|
|
802
|
+
task._stream_queried_data(mapping, local_ids, task._query_db(mapping))
|
|
803
|
+
)
|
|
804
|
+
assert [
|
|
805
|
+
["001000000005", "001000000007"],
|
|
806
|
+
["001000000006", "001000000008"],
|
|
807
|
+
] == records
|
|
808
|
+
|
|
809
|
+
def test_process_lookup_fields_polymorphic(self):
|
|
810
|
+
task = _make_task(
|
|
811
|
+
LoadData,
|
|
812
|
+
{
|
|
813
|
+
"options": {
|
|
814
|
+
"sql_path": Path(__file__).parent
|
|
815
|
+
/ "test_query_db_joins_lookups.sql",
|
|
816
|
+
"mapping": Path(__file__).parent
|
|
817
|
+
/ "test_query_db_joins_lookups_select.yml",
|
|
818
|
+
}
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
polymorphic_fields = {
|
|
822
|
+
"WhoId": {
|
|
823
|
+
"name": "WhoId",
|
|
824
|
+
"referenceTo": ["Contact", "Lead"],
|
|
825
|
+
"relationshipName": "Who",
|
|
826
|
+
},
|
|
827
|
+
"WhatId": {
|
|
828
|
+
"name": "WhatId",
|
|
829
|
+
"referenceTo": ["Account"],
|
|
830
|
+
"relationshipName": "What",
|
|
831
|
+
},
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
expected_fields = [
|
|
835
|
+
"Subject",
|
|
836
|
+
"Who.Contact.FirstName",
|
|
837
|
+
"Who.Contact.LastName",
|
|
838
|
+
"Who.Lead.LastName",
|
|
839
|
+
"WhoId",
|
|
840
|
+
]
|
|
841
|
+
expected_priority_fields_keys = {
|
|
842
|
+
"Who.Contact.FirstName",
|
|
843
|
+
"Who.Contact.LastName",
|
|
844
|
+
"Who.Lead.LastName",
|
|
845
|
+
}
|
|
846
|
+
with mock.patch(
|
|
847
|
+
"cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"
|
|
848
|
+
), mock.patch.object(task, "sf", create=True):
|
|
849
|
+
task._init_mapping()
|
|
850
|
+
with task._init_db():
|
|
851
|
+
task._old_format = mock.Mock(return_value=False)
|
|
852
|
+
mapping = task.mapping["Select Event"]
|
|
853
|
+
fields = mapping.get_load_field_list()
|
|
854
|
+
task.process_lookup_fields(
|
|
855
|
+
mapping=mapping, fields=fields, polymorphic_fields=polymorphic_fields
|
|
856
|
+
)
|
|
857
|
+
assert fields == expected_fields
|
|
858
|
+
assert (
|
|
859
|
+
set(mapping.select_options.priority_fields.keys())
|
|
860
|
+
== expected_priority_fields_keys
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
def test_process_lookup_fields_non_polymorphic(self):
|
|
864
|
+
task = _make_task(
|
|
865
|
+
LoadData,
|
|
866
|
+
{
|
|
867
|
+
"options": {
|
|
868
|
+
"sql_path": Path(__file__).parent
|
|
869
|
+
/ "test_query_db_joins_lookups.sql",
|
|
870
|
+
"mapping": Path(__file__).parent
|
|
871
|
+
/ "test_query_db_joins_lookups_select.yml",
|
|
872
|
+
}
|
|
873
|
+
},
|
|
874
|
+
)
|
|
875
|
+
non_polymorphic_fields = {
|
|
876
|
+
"AccountId": {
|
|
877
|
+
"name": "AccountId",
|
|
878
|
+
"referenceTo": ["Account"],
|
|
879
|
+
"relationshipName": "Account",
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
expected_fields = [
|
|
884
|
+
"FirstName",
|
|
885
|
+
"LastName",
|
|
886
|
+
"Account.Name",
|
|
887
|
+
"Account.AccountNumber",
|
|
888
|
+
"AccountId",
|
|
889
|
+
]
|
|
890
|
+
expected_priority_fields_keys = {
|
|
891
|
+
"FirstName",
|
|
892
|
+
"Account.Name",
|
|
893
|
+
"Account.AccountNumber",
|
|
894
|
+
}
|
|
895
|
+
with mock.patch(
|
|
896
|
+
"cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"
|
|
897
|
+
), mock.patch.object(task, "sf", create=True):
|
|
898
|
+
task._init_mapping()
|
|
899
|
+
with task._init_db():
|
|
900
|
+
task._old_format = mock.Mock(return_value=False)
|
|
901
|
+
mapping = task.mapping["Select Contact"]
|
|
902
|
+
fields = mapping.get_load_field_list()
|
|
903
|
+
task.process_lookup_fields(
|
|
904
|
+
mapping=mapping,
|
|
905
|
+
fields=fields,
|
|
906
|
+
polymorphic_fields=non_polymorphic_fields,
|
|
907
|
+
)
|
|
908
|
+
assert fields == expected_fields
|
|
909
|
+
assert (
|
|
910
|
+
set(mapping.select_options.priority_fields.keys())
|
|
911
|
+
== expected_priority_fields_keys
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
@responses.activate
|
|
915
|
+
def test_stream_queried_data__adjusts_relative_dates(self):
|
|
916
|
+
mock_describe_calls()
|
|
917
|
+
task = _make_task(
|
|
918
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
919
|
+
)
|
|
920
|
+
task._init_task()
|
|
921
|
+
mapping = MappingStep(
|
|
922
|
+
sf_object="Contact",
|
|
923
|
+
action="insert",
|
|
924
|
+
fields=["Birthdate"],
|
|
925
|
+
anchor_date="2020-07-01",
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
task._query_db = mock.Mock()
|
|
929
|
+
task._query_db.return_value.yield_per = mock.Mock(
|
|
930
|
+
return_value=[
|
|
931
|
+
# Local Id, Loaded Id, EmailBouncedDate
|
|
932
|
+
["001000000001", "2020-07-10"],
|
|
933
|
+
["001000000003", None],
|
|
934
|
+
]
|
|
935
|
+
)
|
|
936
|
+
local_ids = io.StringIO()
|
|
937
|
+
records = list(
|
|
938
|
+
task._stream_queried_data(mapping, local_ids, task._query_db(mapping))
|
|
939
|
+
)
|
|
940
|
+
assert [[(date.today() + timedelta(days=9)).isoformat()], [None]] == records
|
|
941
|
+
|
|
942
|
+
def test_get_statics(self):
|
|
943
|
+
task = _make_task(
|
|
944
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
945
|
+
)
|
|
946
|
+
task.sf = mock.Mock()
|
|
947
|
+
task.sf.query.return_value = {"records": [{"Id": "012000000000000"}]}
|
|
948
|
+
|
|
949
|
+
assert ["Technology", "012000000000000"] == task._get_statics(
|
|
950
|
+
MappingStep(
|
|
951
|
+
sf_object="Account",
|
|
952
|
+
fields={"Id": "sf_id", "Name": "Name"},
|
|
953
|
+
static={"Industry": "Technology"},
|
|
954
|
+
record_type="Organization",
|
|
955
|
+
)
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
def test_get_statics_record_type_not_matched(self):
|
|
959
|
+
task = _make_task(
|
|
960
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
961
|
+
)
|
|
962
|
+
task.sf = mock.Mock()
|
|
963
|
+
task.sf.query.return_value = {"records": []}
|
|
964
|
+
with pytest.raises(BulkDataException) as e:
|
|
965
|
+
task._get_statics(
|
|
966
|
+
MappingStep(
|
|
967
|
+
sf_object="Account",
|
|
968
|
+
action="insert",
|
|
969
|
+
fields={"Id": "sf_id", "Name": "Name"},
|
|
970
|
+
static={"Industry": "Technology"},
|
|
971
|
+
record_type="Organization",
|
|
972
|
+
)
|
|
973
|
+
),
|
|
974
|
+
assert "RecordType" in str(e.value)
|
|
975
|
+
|
|
976
|
+
def test_query_db__joins_self_lookups(self):
|
|
977
|
+
"""SQL file in Old Format"""
|
|
978
|
+
_validate_query_for_mapping_step(
|
|
979
|
+
sql_path=Path(__file__).parent / "test_query_db__joins_self_lookups.sql",
|
|
980
|
+
mapping=Path(__file__).parent / "test_query_db__joins_self_lookups.yml",
|
|
981
|
+
mapping_step_name="Update Accounts",
|
|
982
|
+
expected="""SELECT accounts.id AS accounts_id, accounts."Name" AS "accounts_Name", cumulusci_id_table_1.sf_id AS cumulusci_id_table_1_sf_id FROM accounts LEFT OUTER JOIN cumulusci_id_table AS cumulusci_id_table_1 ON cumulusci_id_table_1.id = ? || cast(accounts.parent_id as varchar) ORDER BY accounts.parent_id""",
|
|
983
|
+
old_format=True,
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
def test_query_db__joins_select_lookups(self):
|
|
987
|
+
"""SQL File in New Format (Select)"""
|
|
988
|
+
_validate_query_for_mapping_step(
|
|
989
|
+
sql_path=Path(__file__).parent / "test_query_db_joins_lookups.sql",
|
|
990
|
+
mapping=Path(__file__).parent / "test_query_db_joins_lookups_select.yml",
|
|
991
|
+
mapping_step_name="Select Event",
|
|
992
|
+
expected='''SELECT events.id AS events_id, events."subject" AS "events_subject", "whoid_contacts_alias"."firstname" AS "whoid_contacts_alias_firstname", "whoid_contacts_alias"."lastname" AS "whoid_contacts_alias_lastname", "whoid_leads_alias"."lastname" AS "whoid_leads_alias_lastname", cumulusci_id_table_1.sf_id AS cumulusci_id_table_1_sf_id FROM events LEFT OUTER JOIN contacts AS "whoid_contacts_alias" ON "whoid_contacts_alias".id=events."whoid" LEFT OUTER JOIN leads AS "whoid_leads_alias" ON "whoid_leads_alias".id=events."whoid" LEFT OUTER JOIN cumulusci_id_table AS cumulusci_id_table_1 ON cumulusci_id_table_1.id=? || cast(events."whoid" as varchar) ORDER BY events."whoid"''',
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
def test_query_db__joins_polymorphic_lookups(self):
|
|
996
|
+
"""SQL File in New Format (Polymorphic)"""
|
|
997
|
+
_validate_query_for_mapping_step(
|
|
998
|
+
sql_path=Path(__file__).parent / "test_query_db_joins_lookups.sql",
|
|
999
|
+
mapping=Path(__file__).parent / "test_query_db_joins_lookups.yml",
|
|
1000
|
+
mapping_step_name="Update Event",
|
|
1001
|
+
expected="""SELECT events.id AS events_id, events."Subject" AS "events_Subject", cumulusci_id_table_1.sf_id AS cumulusci_id_table_1_sf_id FROM events LEFT OUTER JOIN cumulusci_id_table AS cumulusci_id_table_1 ON cumulusci_id_table_1.id = ? || cast(events."WhoId" as varchar) ORDER BY events."WhoId" """,
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
@responses.activate
|
|
1005
|
+
def test_query_db__person_accounts_enabled__account_mapping(self):
|
|
1006
|
+
responses.add(
|
|
1007
|
+
method="GET",
|
|
1008
|
+
url="https://example.com/services/data",
|
|
1009
|
+
json=[{"version": CURRENT_SF_API_VERSION}],
|
|
1010
|
+
status=200,
|
|
1011
|
+
)
|
|
1012
|
+
responses.add(
|
|
1013
|
+
method="GET",
|
|
1014
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Account/describe",
|
|
1015
|
+
json={"fields": [{"name": "Id"}, {"name": "IsPersonAccount"}]},
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
_validate_query_for_mapping_step(
|
|
1019
|
+
sql_path=Path(__file__).parent / "person_accounts.sql",
|
|
1020
|
+
mapping=Path(__file__).parent / "update_person_accounts.yml",
|
|
1021
|
+
mapping_step_name="Update Account",
|
|
1022
|
+
expected="""SELECT
|
|
1023
|
+
"Account".id AS "Account_id",
|
|
1024
|
+
"Account"."FirstName" AS "Account_FirstName",
|
|
1025
|
+
"Account"."LastName" AS "Account_LastName",
|
|
1026
|
+
"Account"."PersonMailingStreet" AS "Account_PersonMailingStreet",
|
|
1027
|
+
"Account"."PersonMailingCity" AS "Account_PersonMailingCity",
|
|
1028
|
+
"Account"."PersonMailingState" AS "Account_PersonMailingState",
|
|
1029
|
+
"Account"."PersonMailingCountry" AS "Account_PersonMailingCountry",
|
|
1030
|
+
"Account"."PersonMailingPostalCode" AS "Account_PersonMailingPostalCode",
|
|
1031
|
+
"Account"."PersonEmail" AS "Account_PersonEmail",
|
|
1032
|
+
"Account"."Phone" AS "Account_Phone",
|
|
1033
|
+
"Account"."PersonMobilePhone" AS "Account_PersonMobilePhone"
|
|
1034
|
+
FROM "Account"
|
|
1035
|
+
""",
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
@responses.activate
|
|
1039
|
+
def test_query_db__person_accounts_disabled__account_mapping(self):
|
|
1040
|
+
responses.add(
|
|
1041
|
+
method="GET",
|
|
1042
|
+
url="https://example.com/services/data",
|
|
1043
|
+
json=[{"version": CURRENT_SF_API_VERSION}],
|
|
1044
|
+
status=200,
|
|
1045
|
+
)
|
|
1046
|
+
responses.add(
|
|
1047
|
+
method="GET",
|
|
1048
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/sobjects/Account/describe",
|
|
1049
|
+
json={"fields": [{"name": "Id"}]},
|
|
1050
|
+
)
|
|
1051
|
+
with pytest.raises(BulkDataException) as e:
|
|
1052
|
+
_validate_query_for_mapping_step(
|
|
1053
|
+
sql_path=Path(__file__).parent / "person_accounts.sql",
|
|
1054
|
+
mapping=Path(__file__).parent / "update_person_accounts.yml",
|
|
1055
|
+
mapping_step_name="Update Account",
|
|
1056
|
+
expected="",
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
assert "Person Account" in str(e.value)
|
|
1060
|
+
|
|
1061
|
+
# This test should be rewritten into a light-mocking style but creating the
|
|
1062
|
+
# sample data is non-trivial work that might require some collaboration
|
|
1063
|
+
@mock.patch("cumulusci.tasks.bulkdata.query_transformers.aliased")
|
|
1064
|
+
def test_query_db__person_accounts_enabled__contact_mapping(self, aliased):
|
|
1065
|
+
task = _make_task(
|
|
1066
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
1067
|
+
)
|
|
1068
|
+
model = mock.Mock()
|
|
1069
|
+
task.models = {"contacts": model}
|
|
1070
|
+
task.metadata = mock.Mock()
|
|
1071
|
+
task.metadata.tables = {
|
|
1072
|
+
"cumulusci_id_table": mock.Mock(),
|
|
1073
|
+
}
|
|
1074
|
+
task.session = mock.Mock()
|
|
1075
|
+
task._can_load_person_accounts = mock.Mock(return_value=True)
|
|
1076
|
+
# task._filter_out_person_account_records = mock.Mock() # TODO: Replace this with a better test
|
|
1077
|
+
|
|
1078
|
+
# Make mock query chainable
|
|
1079
|
+
task.session.query.return_value = task.session.query
|
|
1080
|
+
task.session.query.filter.return_value = task.session.query
|
|
1081
|
+
task.session.query.outerjoin.return_value = task.session.query
|
|
1082
|
+
task.session.query.order_by.return_value = task.session.query
|
|
1083
|
+
task.session.query.add_columns.return_value = task.session.query
|
|
1084
|
+
|
|
1085
|
+
model.__table__ = mock.Mock()
|
|
1086
|
+
model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
1087
|
+
columns = {
|
|
1088
|
+
"sf_id": mock.Mock(),
|
|
1089
|
+
"name": mock.Mock(),
|
|
1090
|
+
"IsPersonAccount": mock.Mock(),
|
|
1091
|
+
}
|
|
1092
|
+
model.__table__.columns = columns
|
|
1093
|
+
|
|
1094
|
+
mapping = MappingStep(
|
|
1095
|
+
sf_object="Contact",
|
|
1096
|
+
table="contacts",
|
|
1097
|
+
action=DataOperationType.UPDATE,
|
|
1098
|
+
fields={"Id": "sf_id", "Name": "name"},
|
|
1099
|
+
lookups={
|
|
1100
|
+
"ParentId": MappingLookup(
|
|
1101
|
+
table="accounts", key_field="parent_id", name="ParentId"
|
|
1102
|
+
)
|
|
1103
|
+
},
|
|
1104
|
+
)
|
|
1105
|
+
|
|
1106
|
+
query = task._query_db(mapping)
|
|
1107
|
+
assert (
|
|
1108
|
+
task.session.query == query
|
|
1109
|
+
) # check that query chaining from above worked.
|
|
1110
|
+
|
|
1111
|
+
query_columns, added_filters = _inspect_query(query)
|
|
1112
|
+
# Validate that the column set is accurate
|
|
1113
|
+
assert query_columns == (
|
|
1114
|
+
model.sf_id,
|
|
1115
|
+
model.__table__.columns["name"],
|
|
1116
|
+
aliased.return_value.columns.sf_id,
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
# Validate person contact records WERE filtered out
|
|
1120
|
+
filter_out_contacts, *rest = added_filters
|
|
1121
|
+
assert not rest
|
|
1122
|
+
assert filter_out_contacts.right.value == "false"
|
|
1123
|
+
assert (
|
|
1124
|
+
added_filters[0].left.clause_expr.element.clauses[0].value
|
|
1125
|
+
== columns["IsPersonAccount"]
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
# This test should be rewritten into a light-mocking style but creating the
|
|
1129
|
+
# sample data is non-trivial work that might require some collaboration
|
|
1130
|
+
@mock.patch("cumulusci.tasks.bulkdata.query_transformers.aliased")
|
|
1131
|
+
def test_query_db__person_accounts_disabled__contact_mapping(self, aliased):
|
|
1132
|
+
task = _make_task(
|
|
1133
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
1134
|
+
)
|
|
1135
|
+
model = mock.Mock()
|
|
1136
|
+
task.models = {"contacts": model}
|
|
1137
|
+
task.metadata = mock.Mock()
|
|
1138
|
+
task.metadata.tables = {
|
|
1139
|
+
"cumulusci_id_table": mock.Mock(),
|
|
1140
|
+
}
|
|
1141
|
+
task.session = mock.Mock()
|
|
1142
|
+
task._can_load_person_accounts = mock.Mock(return_value=False)
|
|
1143
|
+
# task._filter_out_person_account_records = mock.Mock() # TODO: Replace this with a better test
|
|
1144
|
+
|
|
1145
|
+
# Make mock query chainable
|
|
1146
|
+
task.session.query.return_value = task.session.query
|
|
1147
|
+
task.session.query.filter.return_value = task.session.query
|
|
1148
|
+
task.session.query.outerjoin.return_value = task.session.query
|
|
1149
|
+
task.session.query.order_by.return_value = task.session.query
|
|
1150
|
+
task.session.query.add_columns.return_value = task.session.query
|
|
1151
|
+
|
|
1152
|
+
model.__table__ = mock.Mock()
|
|
1153
|
+
model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
1154
|
+
columns = {
|
|
1155
|
+
"sf_id": mock.Mock(name="contacts.sf_id"),
|
|
1156
|
+
"name": mock.Mock(name="contacts.name"),
|
|
1157
|
+
"IsPersonAccount": mock.Mock(name="contacts.IsPersonAccount"),
|
|
1158
|
+
}
|
|
1159
|
+
model.__table__.columns = columns
|
|
1160
|
+
|
|
1161
|
+
mapping = MappingStep(
|
|
1162
|
+
sf_object="Contact",
|
|
1163
|
+
table="contacts",
|
|
1164
|
+
action=DataOperationType.UPDATE,
|
|
1165
|
+
fields={"Id": "sf_id", "Name": "name"},
|
|
1166
|
+
lookups={
|
|
1167
|
+
"ParentId": MappingLookup(
|
|
1168
|
+
table="accounts", key_field="parent_id", name="ParentId"
|
|
1169
|
+
)
|
|
1170
|
+
},
|
|
1171
|
+
)
|
|
1172
|
+
|
|
1173
|
+
query = task._query_db(mapping)
|
|
1174
|
+
|
|
1175
|
+
query_columns, added_filters = _inspect_query(query)
|
|
1176
|
+
|
|
1177
|
+
initialization_columns = task.session.query.mock_calls[0].args
|
|
1178
|
+
added_columns = []
|
|
1179
|
+
for call in task.session.query.mock_calls:
|
|
1180
|
+
name, args, kwargs = call
|
|
1181
|
+
if name == "add_columns":
|
|
1182
|
+
added_columns.extend(args)
|
|
1183
|
+
all_columns = initialization_columns + tuple(added_columns)
|
|
1184
|
+
|
|
1185
|
+
# Validate that the column set is accurate
|
|
1186
|
+
assert all_columns == (
|
|
1187
|
+
model.sf_id,
|
|
1188
|
+
model.__table__.columns["name"],
|
|
1189
|
+
aliased.return_value.columns.sf_id,
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
# Validate person contact records were not filtered out
|
|
1193
|
+
task._can_load_person_accounts.assert_called_once_with(mapping)
|
|
1194
|
+
assert tuple(added_filters) == ()
|
|
1195
|
+
|
|
1196
|
+
# This test should be rewritten into a light-mocking style but creating the
|
|
1197
|
+
# sample data is non-trivial work that might require some collaboration
|
|
1198
|
+
|
|
1199
|
+
@mock.patch("cumulusci.tasks.bulkdata.query_transformers.aliased")
|
|
1200
|
+
def test_query_db__person_accounts_enabled__neither_account_nor_contact_mapping(
|
|
1201
|
+
self, aliased
|
|
1202
|
+
):
|
|
1203
|
+
task = _make_task(
|
|
1204
|
+
LoadData, {"options": {"database_url": "sqlite://", "mapping": "test.yml"}}
|
|
1205
|
+
)
|
|
1206
|
+
model = mock.Mock()
|
|
1207
|
+
task.models = {"requests": model}
|
|
1208
|
+
task.metadata = mock.Mock()
|
|
1209
|
+
task.metadata.tables = {
|
|
1210
|
+
"cumulusci_id_table": mock.Mock(),
|
|
1211
|
+
}
|
|
1212
|
+
task.session = mock.Mock()
|
|
1213
|
+
task._can_load_person_accounts = mock.Mock(return_value=True)
|
|
1214
|
+
# task._filter_out_person_account_records = mock.Mock() # TODO: Replace this with a better test
|
|
1215
|
+
|
|
1216
|
+
# Make mock query chainable
|
|
1217
|
+
task.session.query.return_value = task.session.query
|
|
1218
|
+
task.session.query.filter.return_value = task.session.query
|
|
1219
|
+
task.session.query.outerjoin.return_value = task.session.query
|
|
1220
|
+
task.session.query.order_by.return_value = task.session.query
|
|
1221
|
+
task.session.query.add_columns.return_value = task.session.query
|
|
1222
|
+
|
|
1223
|
+
model.__table__ = mock.Mock()
|
|
1224
|
+
model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
1225
|
+
columns = {"sf_id": mock.Mock(), "name": mock.Mock()}
|
|
1226
|
+
model.__table__.columns = columns
|
|
1227
|
+
|
|
1228
|
+
mapping = MappingStep(
|
|
1229
|
+
sf_object="Request__c",
|
|
1230
|
+
table="requests",
|
|
1231
|
+
action=DataOperationType.UPDATE,
|
|
1232
|
+
fields={"Id": "sf_id", "Name": "name"},
|
|
1233
|
+
lookups={
|
|
1234
|
+
"ParentId": MappingLookup(
|
|
1235
|
+
table="accounts", key_field="parent_id", name="ParentId"
|
|
1236
|
+
)
|
|
1237
|
+
},
|
|
1238
|
+
)
|
|
1239
|
+
|
|
1240
|
+
query = task._query_db(mapping)
|
|
1241
|
+
query_columns, added_filters = _inspect_query(query)
|
|
1242
|
+
|
|
1243
|
+
# Validate that the column set is accurate
|
|
1244
|
+
assert query_columns == (
|
|
1245
|
+
model.sf_id,
|
|
1246
|
+
model.__table__.columns["name"],
|
|
1247
|
+
aliased.return_value.columns.sf_id,
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
# Validate person contact db records had their Name updated as blank
|
|
1251
|
+
task._can_load_person_accounts.assert_not_called()
|
|
1252
|
+
|
|
1253
|
+
# Validate person contact records were not filtered out
|
|
1254
|
+
assert tuple(added_filters) == ()
|
|
1255
|
+
|
|
1256
|
+
def test_initialize_id_table__already_exists(self):
|
|
1257
|
+
task = _make_task(
|
|
1258
|
+
LoadData,
|
|
1259
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1260
|
+
)
|
|
1261
|
+
task.mapping = {}
|
|
1262
|
+
with task._init_db():
|
|
1263
|
+
id_table = Table(
|
|
1264
|
+
"cumulusci_id_table",
|
|
1265
|
+
task.metadata,
|
|
1266
|
+
Column("id", Unicode(255), primary_key=True),
|
|
1267
|
+
)
|
|
1268
|
+
id_table.create()
|
|
1269
|
+
task._initialize_id_table(True)
|
|
1270
|
+
new_id_table = task.metadata.tables["cumulusci_id_table"]
|
|
1271
|
+
assert not (new_id_table is id_table)
|
|
1272
|
+
|
|
1273
|
+
def test_initialize_id_table__already_exists_and_should_not_reset_table(self):
|
|
1274
|
+
task = _make_task(
|
|
1275
|
+
LoadData,
|
|
1276
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1277
|
+
)
|
|
1278
|
+
task.mapping = {}
|
|
1279
|
+
with task._init_db():
|
|
1280
|
+
id_table = Table(
|
|
1281
|
+
"cumulusci_id_table",
|
|
1282
|
+
task.metadata,
|
|
1283
|
+
Column("id", Unicode(255), primary_key=True),
|
|
1284
|
+
)
|
|
1285
|
+
id_table.create()
|
|
1286
|
+
task._initialize_id_table(False)
|
|
1287
|
+
new_id_table = task.metadata.tables["cumulusci_id_table"]
|
|
1288
|
+
assert new_id_table is id_table
|
|
1289
|
+
|
|
1290
|
+
def test_run_task__exception_failure(self):
|
|
1291
|
+
task = _make_task(
|
|
1292
|
+
LoadData,
|
|
1293
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1294
|
+
)
|
|
1295
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
1296
|
+
task._init_mapping = mock.Mock()
|
|
1297
|
+
task._execute_step = mock.Mock(
|
|
1298
|
+
return_value=DataOperationJobResult(
|
|
1299
|
+
DataOperationStatus.JOB_FAILURE, [], 0, 0
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
task._initialize_id_table = mock.Mock()
|
|
1303
|
+
task.mapping = {"Test": MappingStep(sf_object="Account")}
|
|
1304
|
+
|
|
1305
|
+
with pytest.raises(BulkDataException):
|
|
1306
|
+
task()
|
|
1307
|
+
|
|
1308
|
+
def test_process_job_results__insert_success(self):
|
|
1309
|
+
task = _make_task(
|
|
1310
|
+
LoadData,
|
|
1311
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
task.session = mock.MagicMock()
|
|
1315
|
+
task.metadata = mock.MagicMock()
|
|
1316
|
+
task._initialize_id_table = mock.Mock()
|
|
1317
|
+
task.bulk = mock.Mock()
|
|
1318
|
+
task.sf = mock.Mock()
|
|
1319
|
+
|
|
1320
|
+
local_ids = ["1"]
|
|
1321
|
+
|
|
1322
|
+
step = FakeBulkAPIDmlOperation(
|
|
1323
|
+
sobject="Contact",
|
|
1324
|
+
operation=DataOperationType.INSERT,
|
|
1325
|
+
api_options={},
|
|
1326
|
+
context=task,
|
|
1327
|
+
fields=[],
|
|
1328
|
+
)
|
|
1329
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1330
|
+
|
|
1331
|
+
mapping = MappingStep(sf_object="Account")
|
|
1332
|
+
with mock.patch(
|
|
1333
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1334
|
+
) as sql_bulk_insert_from_records:
|
|
1335
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1336
|
+
|
|
1337
|
+
task.session.connection.assert_called_once()
|
|
1338
|
+
sql_bulk_insert_from_records.assert_called_once()
|
|
1339
|
+
task.session.commit.assert_called_once()
|
|
1340
|
+
|
|
1341
|
+
def test_process_job_results__insert_rows_fail(self):
|
|
1342
|
+
task = _make_task(
|
|
1343
|
+
LoadData,
|
|
1344
|
+
{
|
|
1345
|
+
"options": {
|
|
1346
|
+
"database_url": "sqlite://",
|
|
1347
|
+
"mapping": "mapping.yml",
|
|
1348
|
+
"ignore_row_errors": True,
|
|
1349
|
+
}
|
|
1350
|
+
},
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
task.session = mock.Mock()
|
|
1354
|
+
task.metadata = mock.MagicMock()
|
|
1355
|
+
task._initialize_id_table = mock.Mock()
|
|
1356
|
+
task.bulk = mock.Mock()
|
|
1357
|
+
task.sf = mock.Mock()
|
|
1358
|
+
task.logger = mock.Mock()
|
|
1359
|
+
|
|
1360
|
+
local_ids = ["1", "2", "3", "4"]
|
|
1361
|
+
|
|
1362
|
+
step = FakeBulkAPIDmlOperation(
|
|
1363
|
+
sobject="Contact",
|
|
1364
|
+
operation=DataOperationType.INSERT,
|
|
1365
|
+
api_options={},
|
|
1366
|
+
context=task,
|
|
1367
|
+
fields=[],
|
|
1368
|
+
)
|
|
1369
|
+
step.job_result = DataOperationJobResult(
|
|
1370
|
+
DataOperationStatus.ROW_FAILURE, [], 4, 4
|
|
1371
|
+
)
|
|
1372
|
+
step.end = mock.Mock()
|
|
1373
|
+
step.results = [
|
|
1374
|
+
DataOperationResult("001111111111111", False, None),
|
|
1375
|
+
DataOperationResult("001111111111112", False, None),
|
|
1376
|
+
DataOperationResult("001111111111113", False, None),
|
|
1377
|
+
DataOperationResult("001111111111114", False, None),
|
|
1378
|
+
]
|
|
1379
|
+
|
|
1380
|
+
mapping = MappingStep(sf_object="Account", table="Account")
|
|
1381
|
+
with mock.patch(
|
|
1382
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1383
|
+
) as sql_bulk_insert_from_records:
|
|
1384
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1385
|
+
|
|
1386
|
+
task.session.connection.assert_called_once()
|
|
1387
|
+
sql_bulk_insert_from_records.assert_not_called()
|
|
1388
|
+
task.session.commit.assert_called_once()
|
|
1389
|
+
assert len(task.logger.mock_calls) == 4
|
|
1390
|
+
|
|
1391
|
+
def test_process_job_results__update_success(self):
|
|
1392
|
+
task = _make_task(
|
|
1393
|
+
LoadData,
|
|
1394
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
task.session = mock.Mock()
|
|
1398
|
+
task._initialize_id_table = mock.Mock()
|
|
1399
|
+
task.bulk = mock.Mock()
|
|
1400
|
+
task.sf = mock.Mock()
|
|
1401
|
+
|
|
1402
|
+
local_ids = ["1"]
|
|
1403
|
+
|
|
1404
|
+
step = FakeBulkAPIDmlOperation(
|
|
1405
|
+
sobject="Contact",
|
|
1406
|
+
operation=DataOperationType.INSERT,
|
|
1407
|
+
api_options={},
|
|
1408
|
+
context=task,
|
|
1409
|
+
fields=[],
|
|
1410
|
+
)
|
|
1411
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1412
|
+
|
|
1413
|
+
mapping = MappingStep(sf_object="Account", action=DataOperationType.UPDATE)
|
|
1414
|
+
with mock.patch(
|
|
1415
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1416
|
+
) as sql_bulk_insert_from_records:
|
|
1417
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1418
|
+
|
|
1419
|
+
task.session.connection.assert_called_once()
|
|
1420
|
+
task._initialize_id_table.assert_not_called()
|
|
1421
|
+
sql_bulk_insert_from_records.assert_not_called()
|
|
1422
|
+
task.session.commit.assert_not_called()
|
|
1423
|
+
|
|
1424
|
+
def test_process_job_results__exception_failure(self):
|
|
1425
|
+
task = _make_task(
|
|
1426
|
+
LoadData,
|
|
1427
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
task.session = mock.Mock()
|
|
1431
|
+
task.metadata = mock.MagicMock()
|
|
1432
|
+
task._initialize_id_table = mock.Mock()
|
|
1433
|
+
task.bulk = mock.Mock()
|
|
1434
|
+
task.sf = mock.Mock()
|
|
1435
|
+
|
|
1436
|
+
local_ids = ["1"]
|
|
1437
|
+
|
|
1438
|
+
step = FakeBulkAPIDmlOperation(
|
|
1439
|
+
sobject="Contact",
|
|
1440
|
+
operation=DataOperationType.UPDATE,
|
|
1441
|
+
api_options={},
|
|
1442
|
+
context=task,
|
|
1443
|
+
fields=[],
|
|
1444
|
+
)
|
|
1445
|
+
step.results = [DataOperationResult(None, False, "message")]
|
|
1446
|
+
step.end()
|
|
1447
|
+
|
|
1448
|
+
mapping = MappingStep(sf_object="Account", action=DataOperationType.UPDATE)
|
|
1449
|
+
|
|
1450
|
+
with mock.patch(
|
|
1451
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1452
|
+
), pytest.raises(BulkDataException) as e:
|
|
1453
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1454
|
+
|
|
1455
|
+
assert "Error on record with id" in str(e.value)
|
|
1456
|
+
assert "message" in str(e.value)
|
|
1457
|
+
|
|
1458
|
+
def test_process_job_results__person_account_contact_ids__not_updated__mapping_action_not_insert(
|
|
1459
|
+
self,
|
|
1460
|
+
):
|
|
1461
|
+
"""
|
|
1462
|
+
Contact ID table is updated with Contact IDs for person account records
|
|
1463
|
+
only if all:
|
|
1464
|
+
❌ mapping's action is "insert"
|
|
1465
|
+
✅ mapping's sf_object is Contact
|
|
1466
|
+
✅ person accounts is enabled
|
|
1467
|
+
✅ an account_id_lookup is found in the mapping
|
|
1468
|
+
"""
|
|
1469
|
+
|
|
1470
|
+
# ❌ mapping's action is "insert"
|
|
1471
|
+
action = DataOperationType.UPDATE
|
|
1472
|
+
|
|
1473
|
+
# ✅ mapping's sf_object is Contact
|
|
1474
|
+
sf_object = "Contact"
|
|
1475
|
+
|
|
1476
|
+
# ✅ person accounts is enabled
|
|
1477
|
+
can_load_person_accounts = True
|
|
1478
|
+
|
|
1479
|
+
# ✅ an account_id_lookup is found in the mapping
|
|
1480
|
+
account_id_lookup = mock.Mock()
|
|
1481
|
+
|
|
1482
|
+
task = _make_task(
|
|
1483
|
+
LoadData,
|
|
1484
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
task.session = mock.Mock()
|
|
1488
|
+
task._initialize_id_table = mock.Mock()
|
|
1489
|
+
task.bulk = mock.Mock()
|
|
1490
|
+
task.sf = mock.Mock()
|
|
1491
|
+
task._can_load_person_accounts = mock.Mock(
|
|
1492
|
+
return_value=can_load_person_accounts
|
|
1493
|
+
)
|
|
1494
|
+
task._generate_contact_id_map_for_person_accounts = mock.Mock()
|
|
1495
|
+
|
|
1496
|
+
local_ids = ["1"]
|
|
1497
|
+
|
|
1498
|
+
step = FakeBulkAPIDmlOperation(
|
|
1499
|
+
sobject="Contact",
|
|
1500
|
+
operation=DataOperationType.INSERT,
|
|
1501
|
+
api_options={},
|
|
1502
|
+
context=task,
|
|
1503
|
+
fields=[],
|
|
1504
|
+
)
|
|
1505
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1506
|
+
|
|
1507
|
+
mapping = MappingStep(
|
|
1508
|
+
sf_object=sf_object,
|
|
1509
|
+
table="Account",
|
|
1510
|
+
action=action,
|
|
1511
|
+
lookups={},
|
|
1512
|
+
)
|
|
1513
|
+
if account_id_lookup:
|
|
1514
|
+
mapping.lookups["AccountId"] = account_id_lookup
|
|
1515
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"):
|
|
1516
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1517
|
+
|
|
1518
|
+
task._generate_contact_id_map_for_person_accounts.assert_not_called()
|
|
1519
|
+
|
|
1520
|
+
def test_process_job_results__person_account_contact_ids__not_updated__sf_object_not_contact(
|
|
1521
|
+
self,
|
|
1522
|
+
):
|
|
1523
|
+
"""
|
|
1524
|
+
Contact ID table is updated with Contact IDs for person account records
|
|
1525
|
+
only if all:
|
|
1526
|
+
✅ mapping's action is "insert"
|
|
1527
|
+
❌ mapping's sf_object is Contact
|
|
1528
|
+
✅ person accounts is enabled
|
|
1529
|
+
✅ an account_id_lookup is found in the mapping
|
|
1530
|
+
"""
|
|
1531
|
+
|
|
1532
|
+
# ✅ mapping's action is "insert"
|
|
1533
|
+
action = DataOperationType.INSERT
|
|
1534
|
+
|
|
1535
|
+
# ❌ mapping's sf_object is Contact
|
|
1536
|
+
sf_object = "Opportunity"
|
|
1537
|
+
|
|
1538
|
+
# ✅ person accounts is enabled
|
|
1539
|
+
can_load_person_accounts = True
|
|
1540
|
+
|
|
1541
|
+
# ✅ an account_id_lookup is found in the mapping
|
|
1542
|
+
account_id_lookup = mock.Mock()
|
|
1543
|
+
|
|
1544
|
+
task = _make_task(
|
|
1545
|
+
LoadData,
|
|
1546
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
task.session = mock.Mock()
|
|
1550
|
+
task.metadata = mock.MagicMock()
|
|
1551
|
+
task._initialize_id_table = mock.Mock()
|
|
1552
|
+
task.bulk = mock.Mock()
|
|
1553
|
+
task.sf = mock.Mock()
|
|
1554
|
+
task._can_load_person_accounts = mock.Mock(
|
|
1555
|
+
return_value=can_load_person_accounts
|
|
1556
|
+
)
|
|
1557
|
+
task._generate_contact_id_map_for_person_accounts = mock.Mock()
|
|
1558
|
+
|
|
1559
|
+
local_ids = ["1"]
|
|
1560
|
+
|
|
1561
|
+
step = FakeBulkAPIDmlOperation(
|
|
1562
|
+
sobject="Contact",
|
|
1563
|
+
operation=DataOperationType.INSERT,
|
|
1564
|
+
api_options={},
|
|
1565
|
+
context=task,
|
|
1566
|
+
fields=[],
|
|
1567
|
+
)
|
|
1568
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1569
|
+
|
|
1570
|
+
mapping = MappingStep(
|
|
1571
|
+
sf_object=sf_object,
|
|
1572
|
+
table="Account",
|
|
1573
|
+
action=action,
|
|
1574
|
+
lookups={},
|
|
1575
|
+
)
|
|
1576
|
+
if account_id_lookup:
|
|
1577
|
+
mapping.lookups["AccountId"] = account_id_lookup
|
|
1578
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"):
|
|
1579
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1580
|
+
|
|
1581
|
+
task._generate_contact_id_map_for_person_accounts.assert_not_called()
|
|
1582
|
+
|
|
1583
|
+
def test_process_job_results__person_account_contact_ids__not_updated__person_accounts_not_enabled(
|
|
1584
|
+
self,
|
|
1585
|
+
):
|
|
1586
|
+
"""
|
|
1587
|
+
Contact ID table is updated with Contact IDs for person account records
|
|
1588
|
+
only if all:
|
|
1589
|
+
✅ mapping's action is "insert"
|
|
1590
|
+
✅ mapping's sf_object is Contact
|
|
1591
|
+
❌ person accounts is enabled
|
|
1592
|
+
✅ an account_id_lookup is found in the mapping
|
|
1593
|
+
"""
|
|
1594
|
+
|
|
1595
|
+
# ✅ mapping's action is "insert"
|
|
1596
|
+
action = DataOperationType.INSERT
|
|
1597
|
+
|
|
1598
|
+
# ✅ mapping's sf_object is Contact
|
|
1599
|
+
sf_object = "Contact"
|
|
1600
|
+
|
|
1601
|
+
# ❌ person accounts is enabled
|
|
1602
|
+
can_load_person_accounts = False
|
|
1603
|
+
|
|
1604
|
+
# ✅ an account_id_lookup is found in the mapping
|
|
1605
|
+
account_id_lookup = mock.Mock()
|
|
1606
|
+
|
|
1607
|
+
task = _make_task(
|
|
1608
|
+
LoadData,
|
|
1609
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1610
|
+
)
|
|
1611
|
+
|
|
1612
|
+
task.session = mock.MagicMock()
|
|
1613
|
+
task.metadata = mock.MagicMock()
|
|
1614
|
+
task._initialize_id_table = mock.Mock()
|
|
1615
|
+
task.bulk = mock.Mock()
|
|
1616
|
+
task.sf = mock.Mock()
|
|
1617
|
+
task._can_load_person_accounts = mock.Mock(
|
|
1618
|
+
return_value=can_load_person_accounts
|
|
1619
|
+
)
|
|
1620
|
+
task._generate_contact_id_map_for_person_accounts = mock.Mock()
|
|
1621
|
+
|
|
1622
|
+
local_ids = ["1"]
|
|
1623
|
+
|
|
1624
|
+
step = FakeBulkAPIDmlOperation(
|
|
1625
|
+
sobject="Contact",
|
|
1626
|
+
operation=DataOperationType.INSERT,
|
|
1627
|
+
api_options={},
|
|
1628
|
+
context=task,
|
|
1629
|
+
fields=[],
|
|
1630
|
+
)
|
|
1631
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1632
|
+
|
|
1633
|
+
mapping = MappingStep(
|
|
1634
|
+
sf_object=sf_object,
|
|
1635
|
+
table="Account",
|
|
1636
|
+
action=action,
|
|
1637
|
+
lookups={},
|
|
1638
|
+
)
|
|
1639
|
+
if account_id_lookup:
|
|
1640
|
+
mapping.lookups["AccountId"] = account_id_lookup
|
|
1641
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"):
|
|
1642
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1643
|
+
|
|
1644
|
+
task._generate_contact_id_map_for_person_accounts.assert_not_called()
|
|
1645
|
+
|
|
1646
|
+
def test_process_job_results__person_account_contact_ids__not_updated__no_account_id_lookup(
|
|
1647
|
+
self,
|
|
1648
|
+
):
|
|
1649
|
+
"""
|
|
1650
|
+
Contact ID table is updated with Contact IDs for person account records
|
|
1651
|
+
only if all:
|
|
1652
|
+
✅ mapping's action is "insert"
|
|
1653
|
+
✅ mapping's sf_object is Contact
|
|
1654
|
+
✅ person accounts is enabled
|
|
1655
|
+
❌ an account_id_lookup is found in the mapping
|
|
1656
|
+
"""
|
|
1657
|
+
|
|
1658
|
+
# ✅ mapping's action is "insert"
|
|
1659
|
+
action = DataOperationType.INSERT
|
|
1660
|
+
|
|
1661
|
+
# ✅ mapping's sf_object is Contact
|
|
1662
|
+
sf_object = "Contact"
|
|
1663
|
+
|
|
1664
|
+
# ✅ person accounts is enabled
|
|
1665
|
+
can_load_person_accounts = True
|
|
1666
|
+
|
|
1667
|
+
# ❌ an account_id_lookup is found in the mapping
|
|
1668
|
+
account_id_lookup = None
|
|
1669
|
+
|
|
1670
|
+
task = _make_task(
|
|
1671
|
+
LoadData,
|
|
1672
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1673
|
+
)
|
|
1674
|
+
|
|
1675
|
+
task.session = mock.Mock()
|
|
1676
|
+
task.metadata = mock.MagicMock()
|
|
1677
|
+
task._initialize_id_table = mock.Mock()
|
|
1678
|
+
task.bulk = mock.Mock()
|
|
1679
|
+
task.sf = mock.Mock()
|
|
1680
|
+
task._can_load_person_accounts = mock.Mock(
|
|
1681
|
+
return_value=can_load_person_accounts
|
|
1682
|
+
)
|
|
1683
|
+
task._generate_contact_id_map_for_person_accounts = mock.Mock()
|
|
1684
|
+
|
|
1685
|
+
local_ids = ["1"]
|
|
1686
|
+
|
|
1687
|
+
step = FakeBulkAPIDmlOperation(
|
|
1688
|
+
sobject="Contact",
|
|
1689
|
+
operation=DataOperationType.INSERT,
|
|
1690
|
+
api_options={},
|
|
1691
|
+
context=task,
|
|
1692
|
+
fields=[],
|
|
1693
|
+
)
|
|
1694
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1695
|
+
|
|
1696
|
+
mapping = MappingStep(
|
|
1697
|
+
sf_object=sf_object,
|
|
1698
|
+
table="Account",
|
|
1699
|
+
action=action,
|
|
1700
|
+
lookups={},
|
|
1701
|
+
)
|
|
1702
|
+
if account_id_lookup:
|
|
1703
|
+
mapping.lookups["AccountId"] = account_id_lookup
|
|
1704
|
+
with mock.patch("cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"):
|
|
1705
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1706
|
+
|
|
1707
|
+
task._generate_contact_id_map_for_person_accounts.assert_not_called()
|
|
1708
|
+
|
|
1709
|
+
def test_process_job_results__person_account_contact_ids__updated(self):
|
|
1710
|
+
"""
|
|
1711
|
+
Contact ID table is updated with Contact IDs for person account records
|
|
1712
|
+
only if all:
|
|
1713
|
+
✅ mapping's action is "insert"
|
|
1714
|
+
✅ mapping's sf_object is Contact
|
|
1715
|
+
✅ person accounts is enabled
|
|
1716
|
+
✅ an account_id_lookup is found in the mapping
|
|
1717
|
+
"""
|
|
1718
|
+
|
|
1719
|
+
# ✅ mapping's action is "insert"
|
|
1720
|
+
action = DataOperationType.INSERT
|
|
1721
|
+
|
|
1722
|
+
# ✅ mapping's sf_object is Contact
|
|
1723
|
+
sf_object = "Contact"
|
|
1724
|
+
|
|
1725
|
+
# ✅ person accounts is enabled
|
|
1726
|
+
can_load_person_accounts = True
|
|
1727
|
+
|
|
1728
|
+
# ✅ an account_id_lookup is found in the mapping
|
|
1729
|
+
account_id_lookup = MappingLookup(table="accounts")
|
|
1730
|
+
|
|
1731
|
+
task = _make_task(
|
|
1732
|
+
LoadData,
|
|
1733
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1734
|
+
)
|
|
1735
|
+
|
|
1736
|
+
task.session = mock.Mock()
|
|
1737
|
+
task.metadata = mock.MagicMock()
|
|
1738
|
+
task._initialize_id_table = mock.Mock()
|
|
1739
|
+
task.bulk = mock.Mock()
|
|
1740
|
+
task.sf = mock.Mock()
|
|
1741
|
+
task._can_load_person_accounts = mock.Mock(
|
|
1742
|
+
return_value=can_load_person_accounts
|
|
1743
|
+
)
|
|
1744
|
+
task._generate_contact_id_map_for_person_accounts = mock.Mock()
|
|
1745
|
+
|
|
1746
|
+
local_ids = ["1"]
|
|
1747
|
+
|
|
1748
|
+
step = FakeBulkAPIDmlOperation(
|
|
1749
|
+
sobject="Contact",
|
|
1750
|
+
operation=DataOperationType.INSERT,
|
|
1751
|
+
api_options={},
|
|
1752
|
+
context=task,
|
|
1753
|
+
fields=[],
|
|
1754
|
+
)
|
|
1755
|
+
step.results = [DataOperationResult("001111111111111", True, None)]
|
|
1756
|
+
|
|
1757
|
+
mapping = MappingStep(
|
|
1758
|
+
sf_object=sf_object,
|
|
1759
|
+
table="Account",
|
|
1760
|
+
action=action,
|
|
1761
|
+
lookups={"AccountId": account_id_lookup},
|
|
1762
|
+
)
|
|
1763
|
+
with mock.patch(
|
|
1764
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1765
|
+
) as sql_bulk_insert_from_records:
|
|
1766
|
+
task._process_job_results(mapping, step, local_ids)
|
|
1767
|
+
|
|
1768
|
+
task._generate_contact_id_map_for_person_accounts.assert_called_once_with(
|
|
1769
|
+
mapping, mapping.lookups["AccountId"], task.session.connection.return_value
|
|
1770
|
+
)
|
|
1771
|
+
assert task._old_format is True
|
|
1772
|
+
sql_bulk_insert_from_records.assert_called_with(
|
|
1773
|
+
connection=task.session.connection.return_value,
|
|
1774
|
+
table=task.metadata.tables[task._initialize_id_table.return_value],
|
|
1775
|
+
columns=("id", "sf_id"),
|
|
1776
|
+
record_iterable=task._generate_contact_id_map_for_person_accounts.return_value,
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
def test_generate_results_id_map__success(self):
|
|
1780
|
+
task = _make_task(
|
|
1781
|
+
LoadData,
|
|
1782
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
1783
|
+
)
|
|
1784
|
+
|
|
1785
|
+
task.session = mock.MagicMock()
|
|
1786
|
+
task.metadata = mock.MagicMock()
|
|
1787
|
+
step = mock.Mock()
|
|
1788
|
+
step.get_results.return_value = iter(
|
|
1789
|
+
[
|
|
1790
|
+
DataOperationResult("001000000000000", True, None, True),
|
|
1791
|
+
DataOperationResult("001000000000001", True, None, True),
|
|
1792
|
+
DataOperationResult("001000000000002", True, None, True),
|
|
1793
|
+
]
|
|
1794
|
+
)
|
|
1795
|
+
|
|
1796
|
+
sf_id_list = task._generate_results_id_map(
|
|
1797
|
+
step, ["001000000000009", "001000000000010", "001000000000011"]
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
assert sf_id_list == [
|
|
1801
|
+
["001000000000009", "001000000000000"],
|
|
1802
|
+
["001000000000010", "001000000000001"],
|
|
1803
|
+
["001000000000011", "001000000000002"],
|
|
1804
|
+
]
|
|
1805
|
+
|
|
1806
|
+
def test_generate_results_id_map__exception_failure_without_rollback(self):
|
|
1807
|
+
task = _make_task(
|
|
1808
|
+
LoadData,
|
|
1809
|
+
{
|
|
1810
|
+
"options": {
|
|
1811
|
+
"database_url": "sqlite://",
|
|
1812
|
+
"mapping": "mapping.yml",
|
|
1813
|
+
"enable_rollback": False,
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
)
|
|
1817
|
+
|
|
1818
|
+
task.metadata = mock.MagicMock()
|
|
1819
|
+
step = mock.Mock()
|
|
1820
|
+
step.get_results.return_value = iter(
|
|
1821
|
+
[
|
|
1822
|
+
DataOperationResult("001000000000000", True, None, True),
|
|
1823
|
+
DataOperationResult(None, False, "error", False),
|
|
1824
|
+
DataOperationResult("001000000000002", True, None, True),
|
|
1825
|
+
]
|
|
1826
|
+
)
|
|
1827
|
+
|
|
1828
|
+
with pytest.raises(BulkDataException) as e:
|
|
1829
|
+
list(
|
|
1830
|
+
task._generate_results_id_map(
|
|
1831
|
+
step, ["001000000000009", "001000000000010", "001000000000011"]
|
|
1832
|
+
)
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
assert "Error on record" in str(e.value)
|
|
1836
|
+
assert "001000000000010" in str(e.value)
|
|
1837
|
+
|
|
1838
|
+
def test_generate_results_id_map__exception_failure_with_rollback(self):
|
|
1839
|
+
task = _make_task(
|
|
1840
|
+
LoadData,
|
|
1841
|
+
{
|
|
1842
|
+
"options": {
|
|
1843
|
+
"database_url": "sqlite://",
|
|
1844
|
+
"mapping": "mapping.yml",
|
|
1845
|
+
"enable_rollback": True,
|
|
1846
|
+
}
|
|
1847
|
+
},
|
|
1848
|
+
)
|
|
1849
|
+
|
|
1850
|
+
task.metadata = mock.MagicMock()
|
|
1851
|
+
task.session = mock.MagicMock()
|
|
1852
|
+
step = mock.Mock()
|
|
1853
|
+
step.get_results.return_value = iter(
|
|
1854
|
+
[
|
|
1855
|
+
DataOperationResult("001000000000000", True, None, True),
|
|
1856
|
+
DataOperationResult(None, False, "error", False),
|
|
1857
|
+
DataOperationResult("001000000000002", True, None, True),
|
|
1858
|
+
]
|
|
1859
|
+
)
|
|
1860
|
+
|
|
1861
|
+
with pytest.raises(BulkDataException) as e, mock.patch(
|
|
1862
|
+
"cumulusci.tasks.bulkdata.load.Rollback._perform_rollback"
|
|
1863
|
+
) as mock_rollback, mock.patch(
|
|
1864
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1865
|
+
) as mock_insert_records:
|
|
1866
|
+
task._generate_results_id_map(
|
|
1867
|
+
step, ["001000000000009", "001000000000010", "001000000000011"]
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
mock_rollback.assert_called_once()
|
|
1871
|
+
mock_insert_records.assert_called_once()
|
|
1872
|
+
assert "Error on record" in str(e.value)
|
|
1873
|
+
assert "001000000000010" in str(e.value)
|
|
1874
|
+
|
|
1875
|
+
def test_generate_results_id_map__respects_silent_error_flag(self):
|
|
1876
|
+
task = _make_task(
|
|
1877
|
+
LoadData,
|
|
1878
|
+
{
|
|
1879
|
+
"options": {
|
|
1880
|
+
"ignore_row_errors": True,
|
|
1881
|
+
"database_url": "sqlite://",
|
|
1882
|
+
"mapping": "mapping.yml",
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
)
|
|
1886
|
+
task.metadata = mock.MagicMock()
|
|
1887
|
+
step = mock.Mock()
|
|
1888
|
+
step.get_results.return_value = iter(
|
|
1889
|
+
[DataOperationResult(None, False, None)] * 15
|
|
1890
|
+
)
|
|
1891
|
+
|
|
1892
|
+
with mock.patch.object(task.logger, "warning") as warning:
|
|
1893
|
+
sf_id_list = task._generate_results_id_map(
|
|
1894
|
+
step, ["001000000000009", "001000000000010", "001000000000011"] * 15
|
|
1895
|
+
)
|
|
1896
|
+
_ = sf_id_list # generate the errors
|
|
1897
|
+
|
|
1898
|
+
assert len(warning.mock_calls) == task.row_warning_limit + 1 == 11
|
|
1899
|
+
assert "warnings suppressed" in str(warning.mock_calls[-1])
|
|
1900
|
+
|
|
1901
|
+
step = mock.Mock()
|
|
1902
|
+
step.get_results.return_value = iter(
|
|
1903
|
+
[
|
|
1904
|
+
DataOperationResult("001000000000000", True, None),
|
|
1905
|
+
DataOperationResult(None, False, None),
|
|
1906
|
+
DataOperationResult("001000000000002", True, None),
|
|
1907
|
+
]
|
|
1908
|
+
)
|
|
1909
|
+
|
|
1910
|
+
sf_id_list = task._generate_results_id_map(
|
|
1911
|
+
step, ["001000000000009", "001000000000010", "001000000000011"]
|
|
1912
|
+
)
|
|
1913
|
+
|
|
1914
|
+
assert sf_id_list == [
|
|
1915
|
+
["001000000000009", "001000000000000"],
|
|
1916
|
+
["001000000000011", "001000000000002"],
|
|
1917
|
+
]
|
|
1918
|
+
|
|
1919
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
1920
|
+
def test__execute_step__prev_record_values(self, mock_dml):
|
|
1921
|
+
task = _make_task(
|
|
1922
|
+
LoadData,
|
|
1923
|
+
{
|
|
1924
|
+
"options": {
|
|
1925
|
+
"database_url": "sqlite://",
|
|
1926
|
+
"mapping": "mapping.yml",
|
|
1927
|
+
"enable_rollback": True,
|
|
1928
|
+
}
|
|
1929
|
+
},
|
|
1930
|
+
)
|
|
1931
|
+
|
|
1932
|
+
ret_prev_records = [["TestName1", "Id1"], ["TestName2", "Id2"]]
|
|
1933
|
+
ret_columns = ("Name", "Id")
|
|
1934
|
+
conn = mock.Mock()
|
|
1935
|
+
task.session = mock.Mock()
|
|
1936
|
+
task.session.connection.return_value = conn
|
|
1937
|
+
task.metadata = mock.MagicMock()
|
|
1938
|
+
tables = {f"Account_{RollbackType.UPSERT}": "AccountUpsertTable"}
|
|
1939
|
+
task.metadata.tables = tables
|
|
1940
|
+
step = mock.Mock()
|
|
1941
|
+
step.fields = ["Name"]
|
|
1942
|
+
step.sobject = "Account"
|
|
1943
|
+
query = mock.Mock()
|
|
1944
|
+
task.configure_step = mock.Mock()
|
|
1945
|
+
task.configure_step.return_value = (step, query)
|
|
1946
|
+
step.get_prev_record_values.return_value = (ret_prev_records, ret_columns)
|
|
1947
|
+
task._load_record_types = mock.Mock()
|
|
1948
|
+
task._process_job_results = mock.Mock()
|
|
1949
|
+
task._query_db = mock.Mock()
|
|
1950
|
+
with mock.patch(
|
|
1951
|
+
"cumulusci.tasks.bulkdata.load.sql_bulk_insert_from_records"
|
|
1952
|
+
) as mock_insert_records:
|
|
1953
|
+
task._execute_step(
|
|
1954
|
+
MappingStep(
|
|
1955
|
+
**{
|
|
1956
|
+
"sf_object": "Account",
|
|
1957
|
+
"fields": {"Name": "Name"},
|
|
1958
|
+
"action": DataOperationType.UPSERT,
|
|
1959
|
+
"api": "rest",
|
|
1960
|
+
"update_key": ["Name"],
|
|
1961
|
+
}
|
|
1962
|
+
)
|
|
1963
|
+
)
|
|
1964
|
+
|
|
1965
|
+
mock_insert_records.assert_called_once_with(
|
|
1966
|
+
connection=conn,
|
|
1967
|
+
table="AccountUpsertTable",
|
|
1968
|
+
columns=ret_columns,
|
|
1969
|
+
record_iterable=ret_prev_records,
|
|
1970
|
+
)
|
|
1971
|
+
|
|
1972
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
1973
|
+
def test__execute_step__job_failure_rollback(self, mock_dml):
|
|
1974
|
+
task = _make_task(
|
|
1975
|
+
LoadData,
|
|
1976
|
+
{
|
|
1977
|
+
"options": {
|
|
1978
|
+
"database_url": "sqlite://",
|
|
1979
|
+
"mapping": "mapping.yml",
|
|
1980
|
+
"enable_rollback": True,
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
)
|
|
1984
|
+
|
|
1985
|
+
task.session = mock.Mock()
|
|
1986
|
+
task.metadata = mock.MagicMock()
|
|
1987
|
+
step = mock.Mock()
|
|
1988
|
+
query = mock.Mock()
|
|
1989
|
+
step.job_result.status = DataOperationStatus.JOB_FAILURE
|
|
1990
|
+
task.configure_step = mock.Mock()
|
|
1991
|
+
task.configure_step.return_value = (step, query)
|
|
1992
|
+
task._load_record_types = mock.Mock()
|
|
1993
|
+
task._process_job_results = mock.Mock()
|
|
1994
|
+
task._query_db = mock.Mock()
|
|
1995
|
+
with mock.patch(
|
|
1996
|
+
"cumulusci.tasks.bulkdata.load.Rollback._perform_rollback"
|
|
1997
|
+
) as mock_rollback:
|
|
1998
|
+
task._execute_step(
|
|
1999
|
+
MappingStep(
|
|
2000
|
+
**{
|
|
2001
|
+
"sf_object": "Account",
|
|
2002
|
+
"action": DataOperationType.INSERT,
|
|
2003
|
+
"fields": {"Name": "Name"},
|
|
2004
|
+
"api": "rest",
|
|
2005
|
+
}
|
|
2006
|
+
)
|
|
2007
|
+
)
|
|
2008
|
+
mock_rollback.assert_called()
|
|
2009
|
+
|
|
2010
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
2011
|
+
def test_execute_step__record_type_mapping(self, dml_mock):
|
|
2012
|
+
task = _make_task(
|
|
2013
|
+
LoadData,
|
|
2014
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
2015
|
+
)
|
|
2016
|
+
|
|
2017
|
+
task.session = mock.Mock()
|
|
2018
|
+
task._load_record_types = mock.Mock()
|
|
2019
|
+
task._process_job_results = mock.Mock()
|
|
2020
|
+
task._query_db = mock.Mock()
|
|
2021
|
+
|
|
2022
|
+
task._execute_step(
|
|
2023
|
+
MappingStep(
|
|
2024
|
+
**{
|
|
2025
|
+
"sf_object": "Account",
|
|
2026
|
+
"action": "insert",
|
|
2027
|
+
"fields": {"Name": "Name"},
|
|
2028
|
+
}
|
|
2029
|
+
)
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
task._load_record_types.assert_not_called()
|
|
2033
|
+
|
|
2034
|
+
task._execute_step(
|
|
2035
|
+
MappingStep(
|
|
2036
|
+
**{
|
|
2037
|
+
"sf_object": "Account",
|
|
2038
|
+
"action": "insert",
|
|
2039
|
+
"fields": {"Name": "Name", "RecordTypeId": "RecordTypeId"},
|
|
2040
|
+
}
|
|
2041
|
+
)
|
|
2042
|
+
)
|
|
2043
|
+
task._load_record_types.assert_called_once_with(
|
|
2044
|
+
["Account"], task.session.connection.return_value
|
|
2045
|
+
)
|
|
2046
|
+
|
|
2047
|
+
def test_query_db__record_type_mapping(self):
|
|
2048
|
+
_validate_query_for_mapping_step(
|
|
2049
|
+
sql_path="cumulusci/tasks/bulkdata/tests/recordtypes.sql",
|
|
2050
|
+
mapping="cumulusci/tasks/bulkdata/tests/recordtypes.yml",
|
|
2051
|
+
mapping_step_name="Insert Accounts",
|
|
2052
|
+
expected="""SELECT accounts.sf_id AS accounts_sf_id, accounts."Name" AS "accounts_Name", "Account_rt_target_mapping".record_type_id AS "Account_rt_target_mapping_record_type_id"
|
|
2053
|
+
FROM accounts
|
|
2054
|
+
LEFT OUTER JOIN "Account_rt_mapping" ON "Account_rt_mapping".record_type_id = accounts."RecordTypeId"
|
|
2055
|
+
LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Account_rt_mapping".developer_name
|
|
2056
|
+
""",
|
|
2057
|
+
)
|
|
2058
|
+
|
|
2059
|
+
def test_query_db__record_type_mapping__with_ispersontype(self):
|
|
2060
|
+
_validate_query_for_mapping_step(
|
|
2061
|
+
sql_path="cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.sql",
|
|
2062
|
+
mapping="cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml",
|
|
2063
|
+
mapping_step_name="Insert Accounts",
|
|
2064
|
+
expected="""SELECT accounts.sf_id AS accounts_sf_id, accounts."Name" AS "accounts_Name", "Account_rt_target_mapping".record_type_id AS "Account_rt_target_mapping_record_type_id"
|
|
2065
|
+
FROM accounts
|
|
2066
|
+
LEFT OUTER JOIN "Account_rt_mapping" ON "Account_rt_mapping".record_type_id = accounts."RecordTypeId"
|
|
2067
|
+
LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Account_rt_mapping".developer_name
|
|
2068
|
+
AND "account_rt_target_mapping".is_person_type = "account_rt_mapping".is_person_type
|
|
2069
|
+
""",
|
|
2070
|
+
)
|
|
2071
|
+
|
|
2072
|
+
def test_query_db__record_type_mapping_table_from_tablename(self):
|
|
2073
|
+
_validate_query_for_mapping_step(
|
|
2074
|
+
sql_path="cumulusci/tasks/bulkdata/tests/recordtypes_2.sql",
|
|
2075
|
+
mapping="cumulusci/tasks/bulkdata/tests/recordtypes_2.yml",
|
|
2076
|
+
mapping_step_name="Insert Account",
|
|
2077
|
+
expected="""SELECT "Beta".id AS "Beta_id", "Beta"."Name" AS "Beta_Name", "Account_rt_target_mapping".record_type_id AS "Account_rt_target_mapping_record_type_id"
|
|
2078
|
+
FROM "Beta"
|
|
2079
|
+
LEFT OUTER JOIN "Beta_rt_mapping" ON "Beta_rt_mapping".record_type_id = "Beta"."RecordType"
|
|
2080
|
+
LEFT OUTER JOIN "Account_rt_target_mapping" ON "Account_rt_target_mapping".developer_name = "Beta_rt_mapping".developer_name
|
|
2081
|
+
""",
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.automap_base")
|
|
2085
|
+
@responses.activate
|
|
2086
|
+
def test_init_db__record_type_mapping(self, base):
|
|
2087
|
+
base_path = os.path.dirname(__file__)
|
|
2088
|
+
mapping_path = os.path.join(base_path, self.mapping_file)
|
|
2089
|
+
task = _make_task(
|
|
2090
|
+
LoadData,
|
|
2091
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2092
|
+
)
|
|
2093
|
+
|
|
2094
|
+
def create_table_mock(table_name):
|
|
2095
|
+
task.models[table_name] = mock.Mock()
|
|
2096
|
+
|
|
2097
|
+
task._create_record_type_table = mock.Mock(side_effect=create_table_mock)
|
|
2098
|
+
task.models = mock.Mock()
|
|
2099
|
+
task.metadata = mock.Mock()
|
|
2100
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists = (
|
|
2101
|
+
mock.Mock()
|
|
2102
|
+
)
|
|
2103
|
+
mock_describe_calls()
|
|
2104
|
+
|
|
2105
|
+
task._init_task()
|
|
2106
|
+
task._init_mapping()
|
|
2107
|
+
task.mapping["Insert Households"]["fields"]["RecordTypeId"] = "RecordTypeId"
|
|
2108
|
+
with task._init_db():
|
|
2109
|
+
task._create_record_type_table.assert_called_once_with(
|
|
2110
|
+
"Account_rt_target_mapping"
|
|
2111
|
+
)
|
|
2112
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists.assert_called_once_with()
|
|
2113
|
+
|
|
2114
|
+
def test_load_record_types(self):
|
|
2115
|
+
task = _make_task(
|
|
2116
|
+
LoadData,
|
|
2117
|
+
{"options": {"database_url": "sqlite://", "mapping": "mapping.yml"}},
|
|
2118
|
+
)
|
|
2119
|
+
|
|
2120
|
+
conn = mock.Mock()
|
|
2121
|
+
task._extract_record_types = mock.Mock()
|
|
2122
|
+
task.org_config._is_person_accounts_enabled = True
|
|
2123
|
+
task._load_record_types(["Account", "Contact"], conn)
|
|
2124
|
+
task._extract_record_types.assert_has_calls(
|
|
2125
|
+
[
|
|
2126
|
+
mock.call("Account", "Account_rt_target_mapping", conn, True),
|
|
2127
|
+
mock.call("Contact", "Contact_rt_target_mapping", conn, True),
|
|
2128
|
+
]
|
|
2129
|
+
)
|
|
2130
|
+
|
|
2131
|
+
@responses.activate
|
|
2132
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_dml_operation")
|
|
2133
|
+
def test_run__autopk(self, dml_mock):
|
|
2134
|
+
responses.add(
|
|
2135
|
+
method="GET",
|
|
2136
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id+FROM+RecordType+WHERE+SObjectType%3D%27Account%27AND+DeveloperName+%3D+%27HH_Account%27+LIMIT+1",
|
|
2137
|
+
body=json.dumps({"records": [{"Id": "1"}]}),
|
|
2138
|
+
status=200,
|
|
2139
|
+
)
|
|
2140
|
+
|
|
2141
|
+
mapping_file = "mapping_v2.yml"
|
|
2142
|
+
base_path = os.path.dirname(__file__)
|
|
2143
|
+
db_path = os.path.join(base_path, "testdata.db")
|
|
2144
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2145
|
+
with temporary_dir() as d:
|
|
2146
|
+
tmp_db_path = os.path.join(d, "testdata.db")
|
|
2147
|
+
shutil.copyfile(db_path, tmp_db_path)
|
|
2148
|
+
|
|
2149
|
+
task = _make_task(
|
|
2150
|
+
LoadData,
|
|
2151
|
+
{
|
|
2152
|
+
"options": {
|
|
2153
|
+
"database_url": f"sqlite:///{tmp_db_path}",
|
|
2154
|
+
"mapping": mapping_path,
|
|
2155
|
+
"set_recently_viewed": False,
|
|
2156
|
+
}
|
|
2157
|
+
},
|
|
2158
|
+
)
|
|
2159
|
+
task.bulk = mock.Mock()
|
|
2160
|
+
task.sf = mock.Mock()
|
|
2161
|
+
step = FakeBulkAPIDmlOperation(
|
|
2162
|
+
sobject="Contact",
|
|
2163
|
+
operation=DataOperationType.INSERT,
|
|
2164
|
+
api_options={},
|
|
2165
|
+
context=task,
|
|
2166
|
+
fields=[],
|
|
2167
|
+
)
|
|
2168
|
+
dml_mock.return_value = step
|
|
2169
|
+
|
|
2170
|
+
step.results = [
|
|
2171
|
+
DataOperationResult("001000000000000", True, None),
|
|
2172
|
+
DataOperationResult("003000000000000", True, None),
|
|
2173
|
+
DataOperationResult("003000000000001", True, None),
|
|
2174
|
+
]
|
|
2175
|
+
|
|
2176
|
+
mock_describe_calls()
|
|
2177
|
+
task()
|
|
2178
|
+
|
|
2179
|
+
assert step.records == [
|
|
2180
|
+
["TestHousehold", "1"],
|
|
2181
|
+
["Test", "User", "test@example.com", "001000000000000"],
|
|
2182
|
+
["Error", "User", "error@example.com", "001000000000000"],
|
|
2183
|
+
], step.records
|
|
2184
|
+
|
|
2185
|
+
with create_engine(task.options["database_url"]).connect() as c:
|
|
2186
|
+
hh_ids = next(c.execute("SELECT * from cumulusci_id_table"))
|
|
2187
|
+
assert hh_ids == ("households-1", "001000000000000")
|
|
2188
|
+
|
|
2189
|
+
@responses.activate
|
|
2190
|
+
def test_run__complex_lookups(self):
|
|
2191
|
+
mapping_file = "mapping-oid.yml"
|
|
2192
|
+
base_path = os.path.dirname(__file__)
|
|
2193
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2194
|
+
task = _make_task(
|
|
2195
|
+
LoadData,
|
|
2196
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2197
|
+
)
|
|
2198
|
+
mock_describe_calls()
|
|
2199
|
+
task._init_task()
|
|
2200
|
+
task._init_mapping()
|
|
2201
|
+
assert (
|
|
2202
|
+
task.mapping["Insert Accounts"]["lookups"]["ParentId"]["after"]
|
|
2203
|
+
== "Insert Accounts"
|
|
2204
|
+
)
|
|
2205
|
+
task.models = {}
|
|
2206
|
+
task.models["accounts"] = mock.MagicMock()
|
|
2207
|
+
task.models["accounts"].__table__ = mock.MagicMock()
|
|
2208
|
+
task.models["accounts"].__table__.primary_key.columns = mock.MagicMock()
|
|
2209
|
+
task.models["accounts"].__table__.primary_key.columns.keys = mock.Mock(
|
|
2210
|
+
return_value=["Id"]
|
|
2211
|
+
)
|
|
2212
|
+
task._expand_mapping()
|
|
2213
|
+
assert (
|
|
2214
|
+
task.mapping["Insert Accounts"]["lookups"]["ParentId"]["after"]
|
|
2215
|
+
== "Insert Accounts"
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
@responses.activate
|
|
2219
|
+
def test_load__inferred_keyfield_camelcase(self):
|
|
2220
|
+
mapping_file = "mapping-oid.yml"
|
|
2221
|
+
base_path = os.path.dirname(__file__)
|
|
2222
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2223
|
+
task = _make_task(
|
|
2224
|
+
LoadData,
|
|
2225
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2226
|
+
)
|
|
2227
|
+
mock_describe_calls()
|
|
2228
|
+
task._init_task()
|
|
2229
|
+
task._init_mapping()
|
|
2230
|
+
|
|
2231
|
+
class FakeModel:
|
|
2232
|
+
ParentId = mock.MagicMock()
|
|
2233
|
+
|
|
2234
|
+
assert (
|
|
2235
|
+
task.mapping["Insert Accounts"]["lookups"]["ParentId"].get_lookup_key_field(
|
|
2236
|
+
FakeModel()
|
|
2237
|
+
)
|
|
2238
|
+
== "ParentId"
|
|
2239
|
+
)
|
|
2240
|
+
|
|
2241
|
+
@responses.activate
|
|
2242
|
+
def test_load__inferred_keyfield_snakecase(self):
|
|
2243
|
+
mapping_file = "mapping-oid.yml"
|
|
2244
|
+
base_path = os.path.dirname(__file__)
|
|
2245
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2246
|
+
task = _make_task(
|
|
2247
|
+
LoadData,
|
|
2248
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2249
|
+
)
|
|
2250
|
+
mock_describe_calls()
|
|
2251
|
+
task._init_task()
|
|
2252
|
+
task._init_mapping()
|
|
2253
|
+
|
|
2254
|
+
class FakeModel:
|
|
2255
|
+
parent_id = mock.MagicMock()
|
|
2256
|
+
|
|
2257
|
+
assert (
|
|
2258
|
+
task.mapping["Insert Accounts"]["lookups"]["ParentId"].get_lookup_key_field(
|
|
2259
|
+
FakeModel()
|
|
2260
|
+
)
|
|
2261
|
+
== "parent_id"
|
|
2262
|
+
)
|
|
2263
|
+
|
|
2264
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__raises_exception__account(
|
|
2265
|
+
self,
|
|
2266
|
+
):
|
|
2267
|
+
"""
|
|
2268
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2269
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2270
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2271
|
+
an unexpected state.
|
|
2272
|
+
- ✅ An Account or Contact object is mapped
|
|
2273
|
+
- ✅ The corresponding table includes an IsPersonAccount column
|
|
2274
|
+
- ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2275
|
+
- ✅ The org does not have person accounts enabled
|
|
2276
|
+
"""
|
|
2277
|
+
mapping_file = "mapping-oid.yml"
|
|
2278
|
+
base_path = os.path.dirname(__file__)
|
|
2279
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2280
|
+
|
|
2281
|
+
task = _make_task(
|
|
2282
|
+
LoadData,
|
|
2283
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2284
|
+
)
|
|
2285
|
+
|
|
2286
|
+
# ✅ An Account object is mapped
|
|
2287
|
+
mapping = MappingStep(sf_object="Account", table="account")
|
|
2288
|
+
model = mock.Mock()
|
|
2289
|
+
model.__table__ = mock.Mock()
|
|
2290
|
+
|
|
2291
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2292
|
+
task.models = {mapping["table"]: model}
|
|
2293
|
+
|
|
2294
|
+
# ✅ The cooresponding table includes an IsPersonAccount column
|
|
2295
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=True)
|
|
2296
|
+
|
|
2297
|
+
# ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2298
|
+
task.session = mock.Mock()
|
|
2299
|
+
task.session.query.return_value = task.session.query
|
|
2300
|
+
task.session.query.filter.return_value = task.session.query
|
|
2301
|
+
|
|
2302
|
+
assert task.session.query.first.return_value is not None
|
|
2303
|
+
|
|
2304
|
+
# ✅ The org does not have person accounts enabled
|
|
2305
|
+
task.org_config._is_person_accounts_enabled = False
|
|
2306
|
+
|
|
2307
|
+
with pytest.raises(BulkDataException):
|
|
2308
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2309
|
+
|
|
2310
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__raises_exception__contact(
|
|
2311
|
+
self,
|
|
2312
|
+
):
|
|
2313
|
+
"""
|
|
2314
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2315
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2316
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2317
|
+
an unexpected state.
|
|
2318
|
+
- ✅ An Account or Contact object is mapped
|
|
2319
|
+
- ✅ The corresponding table includes an IsPersonAccount column
|
|
2320
|
+
- ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2321
|
+
- ✅ The org does not have person accounts enabled
|
|
2322
|
+
"""
|
|
2323
|
+
mapping_file = "mapping-oid.yml"
|
|
2324
|
+
base_path = os.path.dirname(__file__)
|
|
2325
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2326
|
+
|
|
2327
|
+
task = _make_task(
|
|
2328
|
+
LoadData,
|
|
2329
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2330
|
+
)
|
|
2331
|
+
|
|
2332
|
+
# ✅ A Contact object is mapped
|
|
2333
|
+
mapping = MappingStep(sf_object="Contact", table="contact")
|
|
2334
|
+
model = mock.Mock()
|
|
2335
|
+
model.__table__ = mock.Mock()
|
|
2336
|
+
|
|
2337
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2338
|
+
task.models = {mapping["table"]: model}
|
|
2339
|
+
|
|
2340
|
+
# ✅ The cooresponding table includes an IsPersonAccount column
|
|
2341
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=True)
|
|
2342
|
+
|
|
2343
|
+
# ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2344
|
+
task.session = mock.Mock()
|
|
2345
|
+
task.session.query.return_value = task.session.query
|
|
2346
|
+
task.session.query.filter.return_value = task.session.query
|
|
2347
|
+
|
|
2348
|
+
assert task.session.query.first.return_value is not None
|
|
2349
|
+
|
|
2350
|
+
# ✅ The org does not have person accounts enabled
|
|
2351
|
+
task.org_config._is_person_accounts_enabled = False
|
|
2352
|
+
|
|
2353
|
+
with pytest.raises(BulkDataException):
|
|
2354
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2355
|
+
|
|
2356
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__success_if_org_has_person_accounts_enabled(
|
|
2357
|
+
self,
|
|
2358
|
+
):
|
|
2359
|
+
"""
|
|
2360
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2361
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2362
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2363
|
+
an unexpected state.
|
|
2364
|
+
- ✅ An Account or Contact object is mapped
|
|
2365
|
+
- ✅ The corresponding table includes an IsPersonAccount column
|
|
2366
|
+
- ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2367
|
+
- ❌ The org does not have person accounts enabled
|
|
2368
|
+
"""
|
|
2369
|
+
mapping_file = "mapping-oid.yml"
|
|
2370
|
+
base_path = os.path.dirname(__file__)
|
|
2371
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2372
|
+
|
|
2373
|
+
task = _make_task(
|
|
2374
|
+
LoadData,
|
|
2375
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2376
|
+
)
|
|
2377
|
+
|
|
2378
|
+
# ✅ An Account object is mapped
|
|
2379
|
+
mapping = MappingStep(table="account", sf_object="Account")
|
|
2380
|
+
model = mock.Mock()
|
|
2381
|
+
model.__table__ = mock.Mock()
|
|
2382
|
+
|
|
2383
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2384
|
+
task.models = {mapping["table"]: model}
|
|
2385
|
+
|
|
2386
|
+
# ✅ The cooresponding table includes an IsPersonAccount column
|
|
2387
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=True)
|
|
2388
|
+
|
|
2389
|
+
# ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2390
|
+
task.session = mock.Mock()
|
|
2391
|
+
task.session.query.return_value = task.session.query
|
|
2392
|
+
task.session.query.filter.return_value = task.session.query
|
|
2393
|
+
|
|
2394
|
+
assert task.session.query.first.return_value is not None
|
|
2395
|
+
|
|
2396
|
+
# ❌ The org does has person accounts enabled
|
|
2397
|
+
task.org_config._is_person_accounts_enabled = True
|
|
2398
|
+
|
|
2399
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2400
|
+
|
|
2401
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__success_if_no_person_account_records(
|
|
2402
|
+
self,
|
|
2403
|
+
):
|
|
2404
|
+
"""
|
|
2405
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2406
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2407
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2408
|
+
an unexpected state.
|
|
2409
|
+
- ✅ An Account or Contact object is mapped
|
|
2410
|
+
- ✅ The corresponding table includes an IsPersonAccount column
|
|
2411
|
+
- ❌ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2412
|
+
- ✅ The org does not have person accounts enabled
|
|
2413
|
+
"""
|
|
2414
|
+
mapping_file = "mapping-oid.yml"
|
|
2415
|
+
base_path = os.path.dirname(__file__)
|
|
2416
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2417
|
+
|
|
2418
|
+
task = _make_task(
|
|
2419
|
+
LoadData,
|
|
2420
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2421
|
+
)
|
|
2422
|
+
|
|
2423
|
+
# ✅ An Account object is mapped
|
|
2424
|
+
mapping = MappingStep(sf_object="Account", table="account")
|
|
2425
|
+
model = mock.Mock()
|
|
2426
|
+
model.__table__ = mock.Mock()
|
|
2427
|
+
|
|
2428
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2429
|
+
task.models = {mapping["table"]: model}
|
|
2430
|
+
|
|
2431
|
+
# ✅ The cooresponding table includes an IsPersonAccount column
|
|
2432
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=True)
|
|
2433
|
+
|
|
2434
|
+
# ❌ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2435
|
+
task.session = mock.Mock()
|
|
2436
|
+
task.session.query.return_value = task.session.query
|
|
2437
|
+
task.session.query.filter.return_value = task.session.query
|
|
2438
|
+
task.session.query.first.return_value = None
|
|
2439
|
+
|
|
2440
|
+
assert task.session.query.first.return_value is None
|
|
2441
|
+
|
|
2442
|
+
# ✅ The org does has person accounts enabled
|
|
2443
|
+
task.org_config._is_person_accounts_enabled = True
|
|
2444
|
+
|
|
2445
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2446
|
+
|
|
2447
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__success_if_no_person_account_column(
|
|
2448
|
+
self,
|
|
2449
|
+
):
|
|
2450
|
+
"""
|
|
2451
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2452
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2453
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2454
|
+
an unexpected state.
|
|
2455
|
+
- ✅ An Account or Contact object is mapped
|
|
2456
|
+
- ❌ The corresponding table includes an IsPersonAccount column
|
|
2457
|
+
- ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2458
|
+
- ✅ The org does not have person accounts enabled
|
|
2459
|
+
"""
|
|
2460
|
+
mapping_file = "mapping-oid.yml"
|
|
2461
|
+
base_path = os.path.dirname(__file__)
|
|
2462
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2463
|
+
|
|
2464
|
+
task = _make_task(
|
|
2465
|
+
LoadData,
|
|
2466
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2467
|
+
)
|
|
2468
|
+
|
|
2469
|
+
# ✅ An Account object is mapped
|
|
2470
|
+
mapping = MappingStep(sf_object="Account", table="account")
|
|
2471
|
+
model = mock.Mock()
|
|
2472
|
+
model.__table__ = mock.Mock()
|
|
2473
|
+
|
|
2474
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2475
|
+
task.models = {mapping["table"]: model}
|
|
2476
|
+
|
|
2477
|
+
# ❌ The cooresponding table includes an IsPersonAccount column
|
|
2478
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=False)
|
|
2479
|
+
|
|
2480
|
+
# ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2481
|
+
task.session = mock.Mock()
|
|
2482
|
+
task.session.query.return_value = task.session.query
|
|
2483
|
+
task.session.query.filter.return_value = task.session.query
|
|
2484
|
+
|
|
2485
|
+
assert task.session.query.first.return_value is not None
|
|
2486
|
+
|
|
2487
|
+
# ✅ The org does has person accounts enabled
|
|
2488
|
+
task.org_config._is_person_accounts_enabled = True
|
|
2489
|
+
|
|
2490
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2491
|
+
|
|
2492
|
+
def test_validate_org_has_person_accounts_enabled_if_person_account_data_exists__success_if_no_account_or_contact_not_mapped(
|
|
2493
|
+
self,
|
|
2494
|
+
):
|
|
2495
|
+
"""
|
|
2496
|
+
A BulkDataException is raised because the task will (later) attempt to load
|
|
2497
|
+
person account Account records, but the org does not have person accounts enabled
|
|
2498
|
+
which will result in an Exception from the Bulk Data API or load records in
|
|
2499
|
+
an unexpected state.
|
|
2500
|
+
- ❌ An Account or Contact object is mapped
|
|
2501
|
+
- ✅ The corresponding table includes an IsPersonAccount column
|
|
2502
|
+
- ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2503
|
+
- ✅ The org does not have person accounts enabled
|
|
2504
|
+
"""
|
|
2505
|
+
mapping_file = "mapping-oid.yml"
|
|
2506
|
+
base_path = os.path.dirname(__file__)
|
|
2507
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2508
|
+
|
|
2509
|
+
task = _make_task(
|
|
2510
|
+
LoadData,
|
|
2511
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2512
|
+
)
|
|
2513
|
+
|
|
2514
|
+
# ❌ An Account object is mapped
|
|
2515
|
+
mapping = MappingStep(sf_object="CustomObject__c", table="custom_object")
|
|
2516
|
+
model = mock.Mock()
|
|
2517
|
+
model.__table__ = mock.Mock()
|
|
2518
|
+
|
|
2519
|
+
task.mapping = {"Mapping Step": mapping}
|
|
2520
|
+
task.models = {mapping["table"]: model}
|
|
2521
|
+
|
|
2522
|
+
# ✅ The cooresponding table includes an IsPersonAccount column
|
|
2523
|
+
task._db_has_person_accounts_column = mock.Mock(return_value=True)
|
|
2524
|
+
|
|
2525
|
+
# ✅ There is at least one record in the table with IsPersonAccount equals "true"
|
|
2526
|
+
task.session = mock.Mock()
|
|
2527
|
+
task.session.query.return_value = task.session.query
|
|
2528
|
+
task.session.query.filter.return_value = task.session.query
|
|
2529
|
+
|
|
2530
|
+
assert task.session.query.first.return_value is not None
|
|
2531
|
+
|
|
2532
|
+
# ✅ The org does has person accounts enabled
|
|
2533
|
+
task.org_config._is_person_accounts_enabled = True
|
|
2534
|
+
|
|
2535
|
+
task._validate_org_has_person_accounts_enabled_if_person_account_data_exists()
|
|
2536
|
+
|
|
2537
|
+
def test_db_has_person_accounts_column(self):
|
|
2538
|
+
mapping_file = "mapping-oid.yml"
|
|
2539
|
+
base_path = os.path.dirname(__file__)
|
|
2540
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2541
|
+
|
|
2542
|
+
for columns, expected in [
|
|
2543
|
+
({}, False),
|
|
2544
|
+
({"IsPersonAccount": None}, False),
|
|
2545
|
+
({"IsPersonAccount": "Not None"}, True),
|
|
2546
|
+
]:
|
|
2547
|
+
mapping = MappingStep(sf_object="Account")
|
|
2548
|
+
|
|
2549
|
+
model = mock.Mock()
|
|
2550
|
+
model.__table__ = mock.Mock()
|
|
2551
|
+
model.__table__.columns = columns
|
|
2552
|
+
|
|
2553
|
+
task = _make_task(
|
|
2554
|
+
LoadData,
|
|
2555
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2556
|
+
)
|
|
2557
|
+
task.models = {}
|
|
2558
|
+
task.models[mapping.table] = model
|
|
2559
|
+
|
|
2560
|
+
actual = task._db_has_person_accounts_column(mapping)
|
|
2561
|
+
|
|
2562
|
+
assert expected == actual, f"columns: {columns}"
|
|
2563
|
+
|
|
2564
|
+
# def test_filter_out_person_account_records(self, lower):
|
|
2565
|
+
|
|
2566
|
+
# This method was deleted after bb2a7aa13 because it was way too much
|
|
2567
|
+
# code to test too little code. We'll test Person accounts in context
|
|
2568
|
+
# instead.
|
|
2569
|
+
# Delete this comment whenever it is convenient.
|
|
2570
|
+
@pytest.mark.parametrize("old_format", [True, False])
|
|
2571
|
+
def test_generate_contact_id_map_for_person_accounts(self, old_format):
|
|
2572
|
+
mapping_file = "mapping-oid.yml"
|
|
2573
|
+
base_path = os.path.dirname(__file__)
|
|
2574
|
+
mapping_path = os.path.join(base_path, mapping_file)
|
|
2575
|
+
|
|
2576
|
+
# Set task mocks
|
|
2577
|
+
task = _make_task(
|
|
2578
|
+
LoadData,
|
|
2579
|
+
{"options": {"database_url": "sqlite://", "mapping": mapping_path}},
|
|
2580
|
+
)
|
|
2581
|
+
|
|
2582
|
+
account_model = mock.Mock()
|
|
2583
|
+
contact_model = mock.Mock()
|
|
2584
|
+
task.models = {"accounts": account_model, "contacts": contact_model}
|
|
2585
|
+
task.metadata = mock.Mock()
|
|
2586
|
+
task.metadata.tables = {
|
|
2587
|
+
"accounts": mock.Mock(),
|
|
2588
|
+
"contacts": mock.Mock(),
|
|
2589
|
+
"cumulusci_id_table": mock.Mock(),
|
|
2590
|
+
}
|
|
2591
|
+
task.session = mock.Mock()
|
|
2592
|
+
task.session.query.return_value = task.session.query
|
|
2593
|
+
task.session.query.filter.return_value = task.session.query
|
|
2594
|
+
task.session.query.outerjoin.return_value = task.session.query
|
|
2595
|
+
task.sf = mock.Mock()
|
|
2596
|
+
task._old_format = old_format
|
|
2597
|
+
|
|
2598
|
+
# Set model mocks
|
|
2599
|
+
account_model.__table__ = mock.Mock()
|
|
2600
|
+
account_model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
2601
|
+
account_model.__table__.columns = {
|
|
2602
|
+
"id": mock.Mock(),
|
|
2603
|
+
"sf_id": mock.Mock(),
|
|
2604
|
+
"IsPersonAccount": mock.MagicMock(),
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
account_sf_ids_table = mock.Mock()
|
|
2608
|
+
account_sf_ids_table.columns = {"id": mock.Mock(), "sf_id": mock.Mock()}
|
|
2609
|
+
|
|
2610
|
+
contact_model.__table__ = mock.Mock()
|
|
2611
|
+
contact_model.__table__.primary_key.columns.keys.return_value = ["sf_id"]
|
|
2612
|
+
contact_model.__table__.columns = {
|
|
2613
|
+
"id": mock.Mock(),
|
|
2614
|
+
"sf_id": mock.Mock(),
|
|
2615
|
+
"IsPersonAccount": mock.MagicMock(),
|
|
2616
|
+
"account_id": "string",
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
account_id_lookup = MappingLookup(
|
|
2620
|
+
table="accounts", key_field="account_id", name="AccountId"
|
|
2621
|
+
)
|
|
2622
|
+
account_id_lookup.aliased_table = account_sf_ids_table
|
|
2623
|
+
|
|
2624
|
+
# Calculated values
|
|
2625
|
+
contact_id_column = getattr(
|
|
2626
|
+
contact_model, contact_model.__table__.primary_key.columns.keys()[0]
|
|
2627
|
+
)
|
|
2628
|
+
account_id_column = getattr(
|
|
2629
|
+
contact_model, account_id_lookup.get_lookup_key_field(contact_model)
|
|
2630
|
+
)
|
|
2631
|
+
setattr(
|
|
2632
|
+
contact_model,
|
|
2633
|
+
account_id_lookup.get_lookup_key_field(contact_model),
|
|
2634
|
+
"Contact.account_id",
|
|
2635
|
+
)
|
|
2636
|
+
|
|
2637
|
+
account_sf_ids_table = account_id_lookup["aliased_table"]
|
|
2638
|
+
account_sf_id_column = account_sf_ids_table.columns["sf_id"]
|
|
2639
|
+
|
|
2640
|
+
contact_mapping = MappingStep(
|
|
2641
|
+
sf_object="Contact",
|
|
2642
|
+
table="contacts",
|
|
2643
|
+
action=DataOperationType.UPDATE,
|
|
2644
|
+
fields={
|
|
2645
|
+
"Id": "sf_id",
|
|
2646
|
+
"LastName": "LastName",
|
|
2647
|
+
"IsPersonAccount": "IsPersonAccount",
|
|
2648
|
+
},
|
|
2649
|
+
lookups={"AccountId": account_id_lookup},
|
|
2650
|
+
)
|
|
2651
|
+
|
|
2652
|
+
conn = mock.Mock()
|
|
2653
|
+
conn.execution_options.return_value = conn
|
|
2654
|
+
query_result = conn.execute.return_value
|
|
2655
|
+
|
|
2656
|
+
def get_random_string():
|
|
2657
|
+
return "".join(
|
|
2658
|
+
[random.choice(string.ascii_letters + string.digits) for n in range(18)]
|
|
2659
|
+
)
|
|
2660
|
+
|
|
2661
|
+
# Set records to be queried.
|
|
2662
|
+
chunks = [
|
|
2663
|
+
[
|
|
2664
|
+
{
|
|
2665
|
+
# Table IDs
|
|
2666
|
+
"id": get_random_string(),
|
|
2667
|
+
# Salesforce IDs
|
|
2668
|
+
"sf_id": get_random_string(),
|
|
2669
|
+
"AccountId": get_random_string(),
|
|
2670
|
+
}
|
|
2671
|
+
for i in range(200)
|
|
2672
|
+
],
|
|
2673
|
+
[
|
|
2674
|
+
{
|
|
2675
|
+
# Table IDs
|
|
2676
|
+
"id": get_random_string(),
|
|
2677
|
+
# Salesforce IDs
|
|
2678
|
+
"sf_id": get_random_string(),
|
|
2679
|
+
"AccountId": get_random_string(),
|
|
2680
|
+
}
|
|
2681
|
+
for i in range(4)
|
|
2682
|
+
],
|
|
2683
|
+
]
|
|
2684
|
+
|
|
2685
|
+
expected = []
|
|
2686
|
+
query_result.fetchmany.expected_calls = []
|
|
2687
|
+
task.sf.query_all.expected_calls = []
|
|
2688
|
+
for chunk in chunks:
|
|
2689
|
+
if task._old_format:
|
|
2690
|
+
expected.extend(
|
|
2691
|
+
[
|
|
2692
|
+
(
|
|
2693
|
+
str(contact_mapping.table) + "-" + record["id"],
|
|
2694
|
+
record["sf_id"],
|
|
2695
|
+
)
|
|
2696
|
+
for record in chunk
|
|
2697
|
+
]
|
|
2698
|
+
)
|
|
2699
|
+
else:
|
|
2700
|
+
expected.extend([(record["id"], record["sf_id"]) for record in chunk])
|
|
2701
|
+
|
|
2702
|
+
query_result.fetchmany.expected_calls.append(mock.call(200))
|
|
2703
|
+
|
|
2704
|
+
contact_ids_by_account_sf_id = {
|
|
2705
|
+
record["AccountId"]: record["id"] for record in chunk
|
|
2706
|
+
}
|
|
2707
|
+
task.sf.query_all.expected_calls.append(
|
|
2708
|
+
mock.call(
|
|
2709
|
+
"SELECT Id, AccountId FROM Contact WHERE IsPersonAccount = true AND AccountId IN ('{}')".format(
|
|
2710
|
+
"','".join(contact_ids_by_account_sf_id.keys())
|
|
2711
|
+
)
|
|
2712
|
+
)
|
|
2713
|
+
)
|
|
2714
|
+
|
|
2715
|
+
chunks_index = 0
|
|
2716
|
+
|
|
2717
|
+
def fetchmany(batch_size):
|
|
2718
|
+
nonlocal chunks_index
|
|
2719
|
+
|
|
2720
|
+
assert 200 == batch_size
|
|
2721
|
+
|
|
2722
|
+
# _generate_contact_id_map_for_person_accounts should break if fetchmany returns falsy.
|
|
2723
|
+
return (
|
|
2724
|
+
[(record["id"], record["AccountId"]) for record in chunks[chunks_index]]
|
|
2725
|
+
if chunks_index < len(chunks)
|
|
2726
|
+
else None
|
|
2727
|
+
)
|
|
2728
|
+
|
|
2729
|
+
def query_all(query):
|
|
2730
|
+
nonlocal chunks_index
|
|
2731
|
+
chunk = chunks[chunks_index]
|
|
2732
|
+
|
|
2733
|
+
contact_ids_by_account_sf_id = {
|
|
2734
|
+
record["AccountId"]: record["id"] for record in chunk
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
# query_all is called last; increment to next chunk
|
|
2738
|
+
chunks_index += 1
|
|
2739
|
+
|
|
2740
|
+
assert (
|
|
2741
|
+
query
|
|
2742
|
+
== "SELECT Id, AccountId FROM Contact WHERE IsPersonAccount = true AND AccountId IN ('{}')".format(
|
|
2743
|
+
"','".join(contact_ids_by_account_sf_id.keys())
|
|
2744
|
+
)
|
|
2745
|
+
)
|
|
2746
|
+
|
|
2747
|
+
return {
|
|
2748
|
+
"records": [
|
|
2749
|
+
{"Id": record["sf_id"], "AccountId": record["AccountId"]}
|
|
2750
|
+
for record in chunk
|
|
2751
|
+
]
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
conn.execute.return_value.fetchmany.side_effect = fetchmany
|
|
2755
|
+
task.sf.query_all.side_effect = query_all
|
|
2756
|
+
|
|
2757
|
+
# Execute the test.
|
|
2758
|
+
generator = task._generate_contact_id_map_for_person_accounts(
|
|
2759
|
+
contact_mapping, account_id_lookup, conn
|
|
2760
|
+
)
|
|
2761
|
+
|
|
2762
|
+
actual = [value for value in generator]
|
|
2763
|
+
assert expected == actual
|
|
2764
|
+
|
|
2765
|
+
# Assert query executed
|
|
2766
|
+
task.session.query.assert_called_once_with(
|
|
2767
|
+
contact_id_column, account_sf_id_column
|
|
2768
|
+
)
|
|
2769
|
+
task.session.query.filter.assert_called_once()
|
|
2770
|
+
task.session.query.outerjoin.assert_called_once_with(
|
|
2771
|
+
account_sf_ids_table,
|
|
2772
|
+
account_sf_ids_table.columns["id"] == account_id_column,
|
|
2773
|
+
)
|
|
2774
|
+
conn.execution_options.assert_called_once_with(stream_results=True)
|
|
2775
|
+
conn.execute.assert_called_once_with(task.session.query.statement)
|
|
2776
|
+
|
|
2777
|
+
# Assert chunks processed
|
|
2778
|
+
assert len(chunks) == chunks_index
|
|
2779
|
+
|
|
2780
|
+
query_result.fetchmany.assert_has_calls(query_result.fetchmany.expected_calls)
|
|
2781
|
+
task.sf.query_all.assert_has_calls(task.sf.query_all.expected_calls)
|
|
2782
|
+
|
|
2783
|
+
@responses.activate
|
|
2784
|
+
def test_load_memory_usage(self):
|
|
2785
|
+
responses.add(
|
|
2786
|
+
method="GET",
|
|
2787
|
+
url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/query/?q=SELECT+Id+FROM+RecordType+WHERE+SObjectType%3D%27Account%27AND+DeveloperName+%3D+%27HH_Account%27+LIMIT+1",
|
|
2788
|
+
body=json.dumps({"records": [{"Id": "1"}]}),
|
|
2789
|
+
status=200,
|
|
2790
|
+
)
|
|
2791
|
+
|
|
2792
|
+
base_path = os.path.dirname(__file__)
|
|
2793
|
+
sql_path = os.path.join(base_path, "testdata.sql")
|
|
2794
|
+
mapping_path = os.path.join(base_path, self.mapping_file)
|
|
2795
|
+
|
|
2796
|
+
with temporary_dir() as d:
|
|
2797
|
+
tmp_sql_path = os.path.join(d, "testdata.sql")
|
|
2798
|
+
shutil.copyfile(sql_path, tmp_sql_path)
|
|
2799
|
+
|
|
2800
|
+
class NetworklessLoadData(LoadData):
|
|
2801
|
+
def _query_db(self, mapping):
|
|
2802
|
+
if mapping.sf_object == "Account":
|
|
2803
|
+
return FakeQueryResult(
|
|
2804
|
+
((f"{i}",) for i in range(0, numrecords)), numrecords
|
|
2805
|
+
)
|
|
2806
|
+
elif mapping.sf_object == "Contact":
|
|
2807
|
+
return FakeQueryResult(
|
|
2808
|
+
(
|
|
2809
|
+
(f"{i}", "Test☃", "User", "test@example.com", 0)
|
|
2810
|
+
for i in range(0, numrecords)
|
|
2811
|
+
),
|
|
2812
|
+
numrecords,
|
|
2813
|
+
)
|
|
2814
|
+
|
|
2815
|
+
def _init_task(self):
|
|
2816
|
+
super()._init_task()
|
|
2817
|
+
task.bulk = FakeBulkAPI()
|
|
2818
|
+
|
|
2819
|
+
task = _make_task(
|
|
2820
|
+
NetworklessLoadData,
|
|
2821
|
+
{
|
|
2822
|
+
"options": {
|
|
2823
|
+
"sql_path": tmp_sql_path,
|
|
2824
|
+
"mapping": mapping_path,
|
|
2825
|
+
"set_recently_viewed": False,
|
|
2826
|
+
}
|
|
2827
|
+
},
|
|
2828
|
+
)
|
|
2829
|
+
|
|
2830
|
+
numrecords = 5000
|
|
2831
|
+
|
|
2832
|
+
class FakeQueryResult:
|
|
2833
|
+
def __init__(self, results, numrecords=None):
|
|
2834
|
+
self.results = results
|
|
2835
|
+
if numrecords is None:
|
|
2836
|
+
numrecords = len(self.results)
|
|
2837
|
+
self.numrecords = numrecords
|
|
2838
|
+
|
|
2839
|
+
def yield_per(self, number):
|
|
2840
|
+
return self.results
|
|
2841
|
+
|
|
2842
|
+
def count(self):
|
|
2843
|
+
return self.numrecords
|
|
2844
|
+
|
|
2845
|
+
mock_describe_calls()
|
|
2846
|
+
|
|
2847
|
+
def get_results(self):
|
|
2848
|
+
return (
|
|
2849
|
+
DataOperationResult(i, True, None) for i in range(0, numrecords)
|
|
2850
|
+
)
|
|
2851
|
+
|
|
2852
|
+
def _job_state_from_batches(self, job_id):
|
|
2853
|
+
return DataOperationJobResult(
|
|
2854
|
+
DataOperationStatus.SUCCESS,
|
|
2855
|
+
[],
|
|
2856
|
+
numrecords,
|
|
2857
|
+
0,
|
|
2858
|
+
)
|
|
2859
|
+
|
|
2860
|
+
MEGABYTE = 2**20
|
|
2861
|
+
|
|
2862
|
+
# FIXME: more anlysis about the number below
|
|
2863
|
+
with mock.patch(
|
|
2864
|
+
"cumulusci.tasks.bulkdata.step.BulkJobMixin._job_state_from_batches",
|
|
2865
|
+
_job_state_from_batches,
|
|
2866
|
+
), mock.patch(
|
|
2867
|
+
"cumulusci.tasks.bulkdata.step.BulkApiDmlOperation.get_results",
|
|
2868
|
+
get_results,
|
|
2869
|
+
), assert_max_memory_usage(
|
|
2870
|
+
15 * MEGABYTE
|
|
2871
|
+
):
|
|
2872
|
+
task()
|
|
2873
|
+
|
|
2874
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_org_schema")
|
|
2875
|
+
def test_set_viewed(self, get_org_schema):
|
|
2876
|
+
base_path = os.path.dirname(__file__)
|
|
2877
|
+
task = _make_task(
|
|
2878
|
+
LoadData,
|
|
2879
|
+
{
|
|
2880
|
+
"options": {
|
|
2881
|
+
"sql_path": "test.sql",
|
|
2882
|
+
"mapping": os.path.join(base_path, self.mapping_file),
|
|
2883
|
+
}
|
|
2884
|
+
},
|
|
2885
|
+
)
|
|
2886
|
+
queries = []
|
|
2887
|
+
|
|
2888
|
+
def _query_all(query):
|
|
2889
|
+
queries.append(query)
|
|
2890
|
+
return {
|
|
2891
|
+
"records": [
|
|
2892
|
+
{
|
|
2893
|
+
"SobjectName": "Account",
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
"SobjectName": "Custom__c",
|
|
2897
|
+
},
|
|
2898
|
+
],
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2901
|
+
task.sf = mock.Mock()
|
|
2902
|
+
task.sf.query_all = _query_all
|
|
2903
|
+
task.mapping = {}
|
|
2904
|
+
task.mapping["Insert Households"] = MappingStep(sf_object="Account", fields={})
|
|
2905
|
+
task.mapping["Insert Custom__c"] = MappingStep(sf_object="Custom__c", fields={})
|
|
2906
|
+
|
|
2907
|
+
task._set_viewed()
|
|
2908
|
+
|
|
2909
|
+
get_org_schema.assert_called_with(
|
|
2910
|
+
task.sf,
|
|
2911
|
+
task.org_config,
|
|
2912
|
+
included_objects={"Account", "Custom__c"},
|
|
2913
|
+
force_recache=mock.ANY,
|
|
2914
|
+
)
|
|
2915
|
+
|
|
2916
|
+
assert queries == [
|
|
2917
|
+
"SELECT SObjectName FROM TabDefinition WHERE IsCustom = true AND SObjectName IN ('Custom__c')",
|
|
2918
|
+
"SELECT Id FROM Account ORDER BY CreatedDate DESC LIMIT 1000 FOR VIEW",
|
|
2919
|
+
"SELECT Id FROM Custom__c ORDER BY CreatedDate DESC LIMIT 1000 FOR VIEW",
|
|
2920
|
+
], queries
|
|
2921
|
+
|
|
2922
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.get_org_schema", mock.MagicMock())
|
|
2923
|
+
def test_set_viewed__SOQL_error_1(self):
|
|
2924
|
+
base_path = os.path.dirname(__file__)
|
|
2925
|
+
task = _make_task(
|
|
2926
|
+
LoadData,
|
|
2927
|
+
{
|
|
2928
|
+
"options": {
|
|
2929
|
+
"sql_path": "test.sql",
|
|
2930
|
+
"mapping": os.path.join(base_path, self.mapping_file),
|
|
2931
|
+
}
|
|
2932
|
+
},
|
|
2933
|
+
)
|
|
2934
|
+
|
|
2935
|
+
def _query_all(query):
|
|
2936
|
+
assert 0
|
|
2937
|
+
|
|
2938
|
+
task.sf = mock.Mock()
|
|
2939
|
+
task.sf.query_all = _query_all
|
|
2940
|
+
task.mapping = {}
|
|
2941
|
+
task.mapping["Insert Households"] = MappingStep(sf_object="Account", fields={})
|
|
2942
|
+
task.mapping["Insert Custom__c"] = MappingStep(sf_object="Custom__c", fields={})
|
|
2943
|
+
|
|
2944
|
+
with mock.patch.object(task.logger, "warning") as warning:
|
|
2945
|
+
task._set_viewed()
|
|
2946
|
+
|
|
2947
|
+
assert "custom tabs" in str(warning.mock_calls[0])
|
|
2948
|
+
assert "Account" in str(warning.mock_calls[1])
|
|
2949
|
+
|
|
2950
|
+
def test_set_viewed__exception(self):
|
|
2951
|
+
task = _make_task(
|
|
2952
|
+
LoadData,
|
|
2953
|
+
{
|
|
2954
|
+
"options": {
|
|
2955
|
+
"database_url": "sqlite://",
|
|
2956
|
+
"mapping": "mapping.yml",
|
|
2957
|
+
"set_recently_viewed": True,
|
|
2958
|
+
}
|
|
2959
|
+
},
|
|
2960
|
+
)
|
|
2961
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
2962
|
+
task._init_mapping = mock.Mock()
|
|
2963
|
+
task._initialize_id_table = mock.Mock()
|
|
2964
|
+
task.mapping = {}
|
|
2965
|
+
task.after_steps = {}
|
|
2966
|
+
|
|
2967
|
+
def raise_exception():
|
|
2968
|
+
assert 0, "xyzzy"
|
|
2969
|
+
|
|
2970
|
+
task._set_viewed = raise_exception
|
|
2971
|
+
|
|
2972
|
+
with mock.patch.object(task.logger, "warning") as warning:
|
|
2973
|
+
task()
|
|
2974
|
+
assert "xyzzy" in str(warning.mock_calls[0])
|
|
2975
|
+
|
|
2976
|
+
def test_no_mapping(self):
|
|
2977
|
+
task = _make_task(
|
|
2978
|
+
LoadData,
|
|
2979
|
+
{
|
|
2980
|
+
"options": {
|
|
2981
|
+
"database_url": "sqlite://",
|
|
2982
|
+
}
|
|
2983
|
+
},
|
|
2984
|
+
)
|
|
2985
|
+
task._init_db = mock.Mock(return_value=nullcontext())
|
|
2986
|
+
|
|
2987
|
+
with pytest.raises(TaskOptionsError, match="Mapping file path required"):
|
|
2988
|
+
task()
|
|
2989
|
+
|
|
2990
|
+
@mock.patch("cumulusci.tasks.bulkdata.load.validate_and_inject_mapping")
|
|
2991
|
+
def test_mapping_contains_extra_sobjects(self, _):
|
|
2992
|
+
base_path = os.path.dirname(__file__)
|
|
2993
|
+
mapping_path = os.path.join(base_path, self.mapping_file)
|
|
2994
|
+
task = _make_task(
|
|
2995
|
+
LoadData,
|
|
2996
|
+
{
|
|
2997
|
+
"options": {
|
|
2998
|
+
"mapping": mapping_path,
|
|
2999
|
+
"database_url": "sqlite://",
|
|
3000
|
+
}
|
|
3001
|
+
},
|
|
3002
|
+
)
|
|
3003
|
+
with pytest.raises(BulkDataException):
|
|
3004
|
+
task()
|
|
3005
|
+
|
|
3006
|
+
@responses.activate
|
|
3007
|
+
def test_mapping_file_with_explicit_IsPersonAccount(self, caplog):
|
|
3008
|
+
mock_describe_calls()
|
|
3009
|
+
base_path = Path(__file__).parent
|
|
3010
|
+
mapping_path = base_path / "person_accounts_minimal.yml"
|
|
3011
|
+
task = _make_task(
|
|
3012
|
+
LoadData,
|
|
3013
|
+
{
|
|
3014
|
+
"options": {
|
|
3015
|
+
"mapping": mapping_path,
|
|
3016
|
+
"database_url": "sqlite://",
|
|
3017
|
+
}
|
|
3018
|
+
},
|
|
3019
|
+
)
|
|
3020
|
+
|
|
3021
|
+
task._init_task()
|
|
3022
|
+
task._init_mapping()
|
|
3023
|
+
|
|
3024
|
+
|
|
3025
|
+
class TestLoadDataIntegrationTests:
|
|
3026
|
+
# bulk API not supported by VCR yet
|
|
3027
|
+
@pytest.mark.needs_org()
|
|
3028
|
+
def test_error_result_counting__multi_batches(
|
|
3029
|
+
self, create_task, cumulusci_test_repo_root
|
|
3030
|
+
):
|
|
3031
|
+
task = create_task(
|
|
3032
|
+
LoadData,
|
|
3033
|
+
{
|
|
3034
|
+
"sql_path": cumulusci_test_repo_root / "datasets/bad_sample.sql",
|
|
3035
|
+
"mapping": cumulusci_test_repo_root / "datasets/mapping.yml",
|
|
3036
|
+
},
|
|
3037
|
+
)
|
|
3038
|
+
with mock.patch("cumulusci.tasks.bulkdata.step.DEFAULT_BULK_BATCH_SIZE", 3):
|
|
3039
|
+
task()
|
|
3040
|
+
ret = task.return_values["step_results"]
|
|
3041
|
+
assert ret["Account"]["total_row_errors"] == 1
|
|
3042
|
+
assert ret["Contact"]["total_row_errors"] == 1
|
|
3043
|
+
assert ret["Opportunity"]["total_row_errors"] == 2
|
|
3044
|
+
expected = {
|
|
3045
|
+
"Account": {
|
|
3046
|
+
"sobject": "Account",
|
|
3047
|
+
"status": "Row failure",
|
|
3048
|
+
"job_errors": [],
|
|
3049
|
+
"records_processed": 2,
|
|
3050
|
+
"total_row_errors": 1,
|
|
3051
|
+
"record_type": None,
|
|
3052
|
+
},
|
|
3053
|
+
"Contact": {
|
|
3054
|
+
"sobject": "Contact",
|
|
3055
|
+
"status": "Row failure",
|
|
3056
|
+
"job_errors": [],
|
|
3057
|
+
"records_processed": 2,
|
|
3058
|
+
"total_row_errors": 1,
|
|
3059
|
+
"record_type": None,
|
|
3060
|
+
},
|
|
3061
|
+
"Opportunity": {
|
|
3062
|
+
"sobject": "Opportunity",
|
|
3063
|
+
"status": "Row failure",
|
|
3064
|
+
"job_errors": [],
|
|
3065
|
+
"records_processed": 4,
|
|
3066
|
+
"total_row_errors": 2,
|
|
3067
|
+
"record_type": None,
|
|
3068
|
+
},
|
|
3069
|
+
}
|
|
3070
|
+
assert json.loads(json.dumps(ret)) == expected, json.dumps(ret)
|
|
3071
|
+
|
|
3072
|
+
# bulk API not supported by VCR yet
|
|
3073
|
+
@pytest.mark.needs_org()
|
|
3074
|
+
def test_bulk_batch_size(self, create_task):
|
|
3075
|
+
base_path = os.path.dirname(__file__)
|
|
3076
|
+
sql_path = os.path.join(base_path, "testdata.sql")
|
|
3077
|
+
mapping_path = os.path.join(base_path, "mapping_simple.yml")
|
|
3078
|
+
|
|
3079
|
+
orig_batch = BulkApiDmlOperation._batch
|
|
3080
|
+
counts = {}
|
|
3081
|
+
|
|
3082
|
+
def _batch(self, records, n, *args, **kwargs):
|
|
3083
|
+
records = list(records)
|
|
3084
|
+
if records == [["TestHousehold"]]:
|
|
3085
|
+
counts.setdefault("Account", []).append(n)
|
|
3086
|
+
elif records[0][1] == "User":
|
|
3087
|
+
counts.setdefault("Contact", []).append(n)
|
|
3088
|
+
else:
|
|
3089
|
+
assert 0, "Data in SQL must have changed!"
|
|
3090
|
+
records = list(records)
|
|
3091
|
+
return orig_batch(self, records, n, *args, **kwargs)
|
|
3092
|
+
|
|
3093
|
+
with mock.patch(
|
|
3094
|
+
"cumulusci.tasks.bulkdata.step.BulkApiDmlOperation._batch",
|
|
3095
|
+
_batch,
|
|
3096
|
+
):
|
|
3097
|
+
task = create_task(
|
|
3098
|
+
LoadData,
|
|
3099
|
+
{
|
|
3100
|
+
"sql_path": sql_path,
|
|
3101
|
+
"mapping": mapping_path,
|
|
3102
|
+
},
|
|
3103
|
+
)
|
|
3104
|
+
task()
|
|
3105
|
+
assert counts == {"Account": [10000], "Contact": [1]}
|
|
3106
|
+
|
|
3107
|
+
@pytest.mark.needs_org()
|
|
3108
|
+
def test_recreate_set_recent_bug(
|
|
3109
|
+
self, sf, create_task, cumulusci_test_repo_root, org_config
|
|
3110
|
+
):
|
|
3111
|
+
with get_org_schema(sf, org_config, included_objects=["Account"]):
|
|
3112
|
+
pass
|
|
3113
|
+
|
|
3114
|
+
task = create_task(
|
|
3115
|
+
LoadData,
|
|
3116
|
+
{
|
|
3117
|
+
"sql_path": cumulusci_test_repo_root / "datasets/sample.sql",
|
|
3118
|
+
"mapping": cumulusci_test_repo_root / "datasets/mapping.yml",
|
|
3119
|
+
},
|
|
3120
|
+
)
|
|
3121
|
+
task.logger = mock.Mock()
|
|
3122
|
+
results = task()
|
|
3123
|
+
assert results["set_recently_viewed"] == [
|
|
3124
|
+
("Account", None),
|
|
3125
|
+
("Contact", None),
|
|
3126
|
+
("Opportunity", None),
|
|
3127
|
+
]
|
|
3128
|
+
|
|
3129
|
+
|
|
3130
|
+
def _validate_query_for_mapping_step(
|
|
3131
|
+
sql_path, mapping, mapping_step_name, expected, old_format=False
|
|
3132
|
+
):
|
|
3133
|
+
"""Validate the text of a SQL query"""
|
|
3134
|
+
task = _make_task(
|
|
3135
|
+
LoadData,
|
|
3136
|
+
{
|
|
3137
|
+
"options": {
|
|
3138
|
+
"sql_path": sql_path,
|
|
3139
|
+
"mapping": mapping,
|
|
3140
|
+
}
|
|
3141
|
+
},
|
|
3142
|
+
)
|
|
3143
|
+
with mock.patch(
|
|
3144
|
+
"cumulusci.tasks.bulkdata.load.validate_and_inject_mapping"
|
|
3145
|
+
), mock.patch.object(task, "sf", create=True):
|
|
3146
|
+
task._init_mapping()
|
|
3147
|
+
with task._init_db():
|
|
3148
|
+
task._old_format = mock.Mock(return_value=old_format)
|
|
3149
|
+
query = task._query_db(task.mapping[mapping_step_name])
|
|
3150
|
+
|
|
3151
|
+
def normalize(query):
|
|
3152
|
+
return str(query).replace(" ", "").replace("\n", "").replace("\t", "").lower()
|
|
3153
|
+
|
|
3154
|
+
actual = normalize(query)
|
|
3155
|
+
expected = normalize(expected)
|
|
3156
|
+
if actual != expected:
|
|
3157
|
+
print("ACTUAL", actual)
|
|
3158
|
+
print("EXPECT", expected)
|
|
3159
|
+
print("QUERY", query)
|
|
3160
|
+
assert actual == expected
|
|
3161
|
+
|
|
3162
|
+
|
|
3163
|
+
def _inspect_query(query_mock: mock.Mock):
|
|
3164
|
+
"""Figure out what was done to a mocked query"""
|
|
3165
|
+
initialization_columns = query_mock.mock_calls[0].args
|
|
3166
|
+
added_columns = []
|
|
3167
|
+
added_filters = []
|
|
3168
|
+
for call in query_mock.mock_calls:
|
|
3169
|
+
name, args, kwargs = call
|
|
3170
|
+
if name == "add_columns":
|
|
3171
|
+
added_columns.extend(args)
|
|
3172
|
+
elif name == "filter":
|
|
3173
|
+
added_filters.extend(args)
|
|
3174
|
+
query_columns = initialization_columns + tuple(added_columns)
|
|
3175
|
+
return query_columns, added_filters
|