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.

Files changed (744) hide show
  1. cumulusci/__about__.py +1 -0
  2. cumulusci/__init__.py +22 -0
  3. cumulusci/__main__.py +3 -0
  4. cumulusci/cli/__init__.py +0 -0
  5. cumulusci/cli/cci.py +244 -0
  6. cumulusci/cli/error.py +125 -0
  7. cumulusci/cli/flow.py +185 -0
  8. cumulusci/cli/logger.py +72 -0
  9. cumulusci/cli/org.py +692 -0
  10. cumulusci/cli/plan.py +181 -0
  11. cumulusci/cli/project.py +391 -0
  12. cumulusci/cli/robot.py +116 -0
  13. cumulusci/cli/runtime.py +190 -0
  14. cumulusci/cli/service.py +521 -0
  15. cumulusci/cli/task.py +295 -0
  16. cumulusci/cli/tests/__init__.py +0 -0
  17. cumulusci/cli/tests/test_cci.py +545 -0
  18. cumulusci/cli/tests/test_error.py +170 -0
  19. cumulusci/cli/tests/test_flow.py +276 -0
  20. cumulusci/cli/tests/test_logger.py +25 -0
  21. cumulusci/cli/tests/test_org.py +1438 -0
  22. cumulusci/cli/tests/test_plan.py +245 -0
  23. cumulusci/cli/tests/test_project.py +235 -0
  24. cumulusci/cli/tests/test_robot.py +177 -0
  25. cumulusci/cli/tests/test_runtime.py +197 -0
  26. cumulusci/cli/tests/test_service.py +853 -0
  27. cumulusci/cli/tests/test_task.py +266 -0
  28. cumulusci/cli/tests/test_ui.py +310 -0
  29. cumulusci/cli/tests/test_utils.py +122 -0
  30. cumulusci/cli/tests/utils.py +52 -0
  31. cumulusci/cli/ui.py +234 -0
  32. cumulusci/cli/utils.py +150 -0
  33. cumulusci/conftest.py +181 -0
  34. cumulusci/core/__init__.py +0 -0
  35. cumulusci/core/config/BaseConfig.py +5 -0
  36. cumulusci/core/config/BaseTaskFlowConfig.py +5 -0
  37. cumulusci/core/config/OrgConfig.py +5 -0
  38. cumulusci/core/config/ScratchOrgConfig.py +5 -0
  39. cumulusci/core/config/__init__.py +125 -0
  40. cumulusci/core/config/base_config.py +111 -0
  41. cumulusci/core/config/base_task_flow_config.py +82 -0
  42. cumulusci/core/config/marketing_cloud_service_config.py +83 -0
  43. cumulusci/core/config/oauth2_service_config.py +17 -0
  44. cumulusci/core/config/org_config.py +604 -0
  45. cumulusci/core/config/project_config.py +782 -0
  46. cumulusci/core/config/scratch_org_config.py +251 -0
  47. cumulusci/core/config/sfdx_org_config.py +220 -0
  48. cumulusci/core/config/tests/_test_config_backwards_compatibility.py +33 -0
  49. cumulusci/core/config/tests/test_config.py +1895 -0
  50. cumulusci/core/config/tests/test_config_expensive.py +839 -0
  51. cumulusci/core/config/tests/test_config_util.py +91 -0
  52. cumulusci/core/config/universal_config.py +88 -0
  53. cumulusci/core/config/util.py +18 -0
  54. cumulusci/core/datasets.py +303 -0
  55. cumulusci/core/debug.py +33 -0
  56. cumulusci/core/dependencies/__init__.py +55 -0
  57. cumulusci/core/dependencies/base.py +561 -0
  58. cumulusci/core/dependencies/dependencies.py +273 -0
  59. cumulusci/core/dependencies/github.py +177 -0
  60. cumulusci/core/dependencies/github_resolvers.py +244 -0
  61. cumulusci/core/dependencies/resolvers.py +580 -0
  62. cumulusci/core/dependencies/tests/__init__.py +0 -0
  63. cumulusci/core/dependencies/tests/conftest.py +385 -0
  64. cumulusci/core/dependencies/tests/test_dependencies.py +950 -0
  65. cumulusci/core/dependencies/tests/test_github.py +83 -0
  66. cumulusci/core/dependencies/tests/test_resolvers.py +1027 -0
  67. cumulusci/core/dependencies/utils.py +13 -0
  68. cumulusci/core/enums.py +11 -0
  69. cumulusci/core/exceptions.py +311 -0
  70. cumulusci/core/flowrunner.py +888 -0
  71. cumulusci/core/github.py +665 -0
  72. cumulusci/core/keychain/__init__.py +24 -0
  73. cumulusci/core/keychain/base_project_keychain.py +441 -0
  74. cumulusci/core/keychain/encrypted_file_project_keychain.py +945 -0
  75. cumulusci/core/keychain/environment_project_keychain.py +7 -0
  76. cumulusci/core/keychain/serialization.py +152 -0
  77. cumulusci/core/keychain/subprocess_keychain.py +24 -0
  78. cumulusci/core/keychain/tests/conftest.py +50 -0
  79. cumulusci/core/keychain/tests/test_base_project_keychain.py +299 -0
  80. cumulusci/core/keychain/tests/test_encrypted_file_project_keychain.py +1228 -0
  81. cumulusci/core/metadeploy/__init__.py +0 -0
  82. cumulusci/core/metadeploy/api.py +88 -0
  83. cumulusci/core/metadeploy/plans.py +25 -0
  84. cumulusci/core/metadeploy/tests/test_api.py +276 -0
  85. cumulusci/core/runtime.py +115 -0
  86. cumulusci/core/sfdx.py +162 -0
  87. cumulusci/core/source/__init__.py +16 -0
  88. cumulusci/core/source/github.py +50 -0
  89. cumulusci/core/source/local_folder.py +35 -0
  90. cumulusci/core/source_transforms/__init__.py +0 -0
  91. cumulusci/core/source_transforms/tests/test_transforms.py +1091 -0
  92. cumulusci/core/source_transforms/transforms.py +532 -0
  93. cumulusci/core/tasks.py +404 -0
  94. cumulusci/core/template_utils.py +59 -0
  95. cumulusci/core/tests/__init__.py +0 -0
  96. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_e2e.yaml +215 -0
  97. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_extract_standard_objects.yaml +199 -0
  98. cumulusci/core/tests/cassettes/TestDatasetsE2E.test_datasets_read_explicit_extract_declaration.yaml +3 -0
  99. cumulusci/core/tests/fake_remote_repo/cumulusci.yml +32 -0
  100. cumulusci/core/tests/fake_remote_repo/tasks/directory/example_2.py +6 -0
  101. cumulusci/core/tests/fake_remote_repo/tasks/example.py +43 -0
  102. cumulusci/core/tests/fake_remote_repo_2/cumulusci.yml +11 -0
  103. cumulusci/core/tests/fake_remote_repo_2/tasks/example_3.py +6 -0
  104. cumulusci/core/tests/test_datasets_e2e.py +386 -0
  105. cumulusci/core/tests/test_exceptions.py +11 -0
  106. cumulusci/core/tests/test_flowrunner.py +836 -0
  107. cumulusci/core/tests/test_github.py +942 -0
  108. cumulusci/core/tests/test_sfdx.py +138 -0
  109. cumulusci/core/tests/test_source.py +678 -0
  110. cumulusci/core/tests/test_tasks.py +262 -0
  111. cumulusci/core/tests/test_utils.py +141 -0
  112. cumulusci/core/tests/test_utils_merge_config.py +276 -0
  113. cumulusci/core/tests/test_versions.py +76 -0
  114. cumulusci/core/tests/untrusted_repo_child/cumulusci.yml +7 -0
  115. cumulusci/core/tests/untrusted_repo_child/tasks/untrusted_child.py +6 -0
  116. cumulusci/core/tests/untrusted_repo_parent/cumulusci.yml +26 -0
  117. cumulusci/core/tests/untrusted_repo_parent/tasks/untrusted_parent.py +6 -0
  118. cumulusci/core/tests/utils.py +116 -0
  119. cumulusci/core/tests/yaml/global.yaml +0 -0
  120. cumulusci/core/utils.py +402 -0
  121. cumulusci/core/versions.py +149 -0
  122. cumulusci/cumulusci.yml +1621 -0
  123. cumulusci/files/admin_profile.xml +20 -0
  124. cumulusci/files/delete_excludes.txt +424 -0
  125. cumulusci/files/templates/project/README.md +12 -0
  126. cumulusci/files/templates/project/cumulusci.yml +63 -0
  127. cumulusci/files/templates/project/dot-gitignore +60 -0
  128. cumulusci/files/templates/project/mapping.yml +45 -0
  129. cumulusci/files/templates/project/scratch_def.json +25 -0
  130. cumulusci/oauth/__init__.py +0 -0
  131. cumulusci/oauth/client.py +400 -0
  132. cumulusci/oauth/exceptions.py +9 -0
  133. cumulusci/oauth/salesforce.py +95 -0
  134. cumulusci/oauth/tests/__init__.py +0 -0
  135. cumulusci/oauth/tests/cassettes/test_get_device_code.yaml +22 -0
  136. cumulusci/oauth/tests/cassettes/test_get_device_oauth_token.yaml +74 -0
  137. cumulusci/oauth/tests/test_client.py +308 -0
  138. cumulusci/oauth/tests/test_salesforce.py +46 -0
  139. cumulusci/plugins/__init__.py +3 -0
  140. cumulusci/plugins/plugin_base.py +93 -0
  141. cumulusci/plugins/plugin_loader.py +59 -0
  142. cumulusci/robotframework/CumulusCI.py +340 -0
  143. cumulusci/robotframework/CumulusCI.robot +7 -0
  144. cumulusci/robotframework/Performance.py +165 -0
  145. cumulusci/robotframework/Salesforce.py +936 -0
  146. cumulusci/robotframework/Salesforce.robot +192 -0
  147. cumulusci/robotframework/SalesforceAPI.py +416 -0
  148. cumulusci/robotframework/SalesforcePlaywright.py +220 -0
  149. cumulusci/robotframework/SalesforcePlaywright.robot +40 -0
  150. cumulusci/robotframework/__init__.py +2 -0
  151. cumulusci/robotframework/base_library.py +39 -0
  152. cumulusci/robotframework/faker_mixin.py +89 -0
  153. cumulusci/robotframework/form_handlers.py +222 -0
  154. cumulusci/robotframework/javascript/cci_init.js +34 -0
  155. cumulusci/robotframework/javascript/cumulusci.js +4 -0
  156. cumulusci/robotframework/locator_manager.py +197 -0
  157. cumulusci/robotframework/locators_56.py +88 -0
  158. cumulusci/robotframework/locators_57.py +5 -0
  159. cumulusci/robotframework/pageobjects/BasePageObjects.py +433 -0
  160. cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +246 -0
  161. cumulusci/robotframework/pageobjects/PageObjectLibrary.py +45 -0
  162. cumulusci/robotframework/pageobjects/PageObjects.py +351 -0
  163. cumulusci/robotframework/pageobjects/__init__.py +12 -0
  164. cumulusci/robotframework/pageobjects/baseobjects.py +120 -0
  165. cumulusci/robotframework/perftests/short/collection_perf.robot +105 -0
  166. cumulusci/robotframework/tests/CustomObjectTestPage.py +10 -0
  167. cumulusci/robotframework/tests/FooTestPage.py +8 -0
  168. cumulusci/robotframework/tests/cumulusci/base.robot +40 -0
  169. cumulusci/robotframework/tests/cumulusci/bulkdata.robot +38 -0
  170. cumulusci/robotframework/tests/cumulusci/communities.robot +57 -0
  171. cumulusci/robotframework/tests/cumulusci/datagen.robot +84 -0
  172. cumulusci/robotframework/tests/salesforce/TestLibraryA.py +24 -0
  173. cumulusci/robotframework/tests/salesforce/TestLibraryB.py +20 -0
  174. cumulusci/robotframework/tests/salesforce/TestListener.py +93 -0
  175. cumulusci/robotframework/tests/salesforce/api.robot +178 -0
  176. cumulusci/robotframework/tests/salesforce/browsers.robot +143 -0
  177. cumulusci/robotframework/tests/salesforce/classic.robot +51 -0
  178. cumulusci/robotframework/tests/salesforce/create_contact.robot +59 -0
  179. cumulusci/robotframework/tests/salesforce/faker.robot +68 -0
  180. cumulusci/robotframework/tests/salesforce/forms.robot +172 -0
  181. cumulusci/robotframework/tests/salesforce/label_locator.robot +244 -0
  182. cumulusci/robotframework/tests/salesforce/labels.html +33 -0
  183. cumulusci/robotframework/tests/salesforce/locators.robot +149 -0
  184. cumulusci/robotframework/tests/salesforce/pageobjects/base_pageobjects.robot +100 -0
  185. cumulusci/robotframework/tests/salesforce/pageobjects/example_page_object.py +25 -0
  186. cumulusci/robotframework/tests/salesforce/pageobjects/listing_page.robot +115 -0
  187. cumulusci/robotframework/tests/salesforce/pageobjects/objectmanager.robot +74 -0
  188. cumulusci/robotframework/tests/salesforce/pageobjects/pageobjects.robot +171 -0
  189. cumulusci/robotframework/tests/salesforce/performance.robot +109 -0
  190. cumulusci/robotframework/tests/salesforce/playwright/javascript_keywords.robot +33 -0
  191. cumulusci/robotframework/tests/salesforce/playwright/open_test_browser.robot +48 -0
  192. cumulusci/robotframework/tests/salesforce/playwright/playwright.robot +24 -0
  193. cumulusci/robotframework/tests/salesforce/playwright/ui.robot +32 -0
  194. cumulusci/robotframework/tests/salesforce/populate.robot +89 -0
  195. cumulusci/robotframework/tests/salesforce/test_testlistener.py +37 -0
  196. cumulusci/robotframework/tests/salesforce/ui.robot +361 -0
  197. cumulusci/robotframework/tests/test_cumulusci_library.py +304 -0
  198. cumulusci/robotframework/tests/test_locator_manager.py +158 -0
  199. cumulusci/robotframework/tests/test_pageobjects.py +291 -0
  200. cumulusci/robotframework/tests/test_performance.py +38 -0
  201. cumulusci/robotframework/tests/test_salesforce.py +79 -0
  202. cumulusci/robotframework/tests/test_salesforce_locators.py +73 -0
  203. cumulusci/robotframework/tests/test_template_util.py +53 -0
  204. cumulusci/robotframework/tests/test_utils.py +106 -0
  205. cumulusci/robotframework/utils.py +283 -0
  206. cumulusci/salesforce_api/__init__.py +0 -0
  207. cumulusci/salesforce_api/exceptions.py +23 -0
  208. cumulusci/salesforce_api/filterable_objects.py +96 -0
  209. cumulusci/salesforce_api/mc_soap_envelopes.py +89 -0
  210. cumulusci/salesforce_api/metadata.py +721 -0
  211. cumulusci/salesforce_api/org_schema.py +571 -0
  212. cumulusci/salesforce_api/org_schema_models.py +226 -0
  213. cumulusci/salesforce_api/package_install.py +265 -0
  214. cumulusci/salesforce_api/package_zip.py +301 -0
  215. cumulusci/salesforce_api/rest_deploy.py +148 -0
  216. cumulusci/salesforce_api/retrieve_profile_api.py +301 -0
  217. cumulusci/salesforce_api/soap_envelopes.py +177 -0
  218. cumulusci/salesforce_api/tests/__init__.py +0 -0
  219. cumulusci/salesforce_api/tests/metadata_test_strings.py +24 -0
  220. cumulusci/salesforce_api/tests/test_metadata.py +1015 -0
  221. cumulusci/salesforce_api/tests/test_package_install.py +219 -0
  222. cumulusci/salesforce_api/tests/test_package_zip.py +380 -0
  223. cumulusci/salesforce_api/tests/test_rest_deploy.py +264 -0
  224. cumulusci/salesforce_api/tests/test_retrieve_profile_api.py +337 -0
  225. cumulusci/salesforce_api/tests/test_utils.py +124 -0
  226. cumulusci/salesforce_api/utils.py +51 -0
  227. cumulusci/schema/cumulusci.jsonschema.json +782 -0
  228. cumulusci/tasks/__init__.py +0 -0
  229. cumulusci/tasks/apex/__init__.py +0 -0
  230. cumulusci/tasks/apex/anon.py +157 -0
  231. cumulusci/tasks/apex/batch.py +180 -0
  232. cumulusci/tasks/apex/testrunner.py +835 -0
  233. cumulusci/tasks/apex/tests/cassettes/ManualEditTestApexIntegrationTests.test_run_tests__integration_test.yaml +703 -0
  234. cumulusci/tasks/apex/tests/test_apex_tasks.py +1558 -0
  235. cumulusci/tasks/base_source_control_task.py +17 -0
  236. cumulusci/tasks/bulkdata/__init__.py +15 -0
  237. cumulusci/tasks/bulkdata/base_generate_data_task.py +96 -0
  238. cumulusci/tasks/bulkdata/dates.py +97 -0
  239. cumulusci/tasks/bulkdata/delete.py +156 -0
  240. cumulusci/tasks/bulkdata/extract.py +441 -0
  241. cumulusci/tasks/bulkdata/extract_dataset_utils/calculate_dependencies.py +117 -0
  242. cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +123 -0
  243. cumulusci/tasks/bulkdata/extract_dataset_utils/hardcoded_default_declarations.py +49 -0
  244. cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +283 -0
  245. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +142 -0
  246. cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_synthesize_extract_declarations.py +575 -0
  247. cumulusci/tasks/bulkdata/factory_utils.py +134 -0
  248. cumulusci/tasks/bulkdata/generate.py +4 -0
  249. cumulusci/tasks/bulkdata/generate_and_load_data.py +232 -0
  250. cumulusci/tasks/bulkdata/generate_and_load_data_from_yaml.py +19 -0
  251. cumulusci/tasks/bulkdata/generate_from_yaml.py +183 -0
  252. cumulusci/tasks/bulkdata/generate_mapping.py +434 -0
  253. cumulusci/tasks/bulkdata/generate_mapping_utils/dependency_map.py +169 -0
  254. cumulusci/tasks/bulkdata/generate_mapping_utils/extract_mapping_file_generator.py +45 -0
  255. cumulusci/tasks/bulkdata/generate_mapping_utils/generate_mapping_from_declarations.py +121 -0
  256. cumulusci/tasks/bulkdata/generate_mapping_utils/load_mapping_file_generator.py +127 -0
  257. cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_generator_post_processes.py +53 -0
  258. cumulusci/tasks/bulkdata/generate_mapping_utils/mapping_transforms.py +139 -0
  259. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_extract_mapping_from_declarations.py +135 -0
  260. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_generate_load_mapping_from_declarations.py +330 -0
  261. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_generator_post_processes.py +60 -0
  262. cumulusci/tasks/bulkdata/generate_mapping_utils/tests/test_mapping_transforms.py +188 -0
  263. cumulusci/tasks/bulkdata/load.py +1196 -0
  264. cumulusci/tasks/bulkdata/mapping_parser.py +811 -0
  265. cumulusci/tasks/bulkdata/query_transformers.py +264 -0
  266. cumulusci/tasks/bulkdata/select_utils.py +792 -0
  267. cumulusci/tasks/bulkdata/snowfakery.py +753 -0
  268. cumulusci/tasks/bulkdata/snowfakery_utils/queue_manager.py +478 -0
  269. cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_run_until.py +141 -0
  270. cumulusci/tasks/bulkdata/snowfakery_utils/snowfakery_working_directory.py +53 -0
  271. cumulusci/tasks/bulkdata/snowfakery_utils/subtask_configurator.py +64 -0
  272. cumulusci/tasks/bulkdata/step.py +1242 -0
  273. cumulusci/tasks/bulkdata/tests/__init__.py +0 -0
  274. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_random_strategy.yaml +147 -0
  275. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_annoy_strategy.yaml +123 -0
  276. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy.yaml +313 -0
  277. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_select_and_insert_strategy_bulk.yaml +550 -0
  278. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_similarity_strategy.yaml +175 -0
  279. cumulusci/tasks/bulkdata/tests/cassettes/TestSelect.test_select_standard_strategy.yaml +147 -0
  280. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__multiple_needed.yaml +69 -0
  281. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__none_needed.yaml +22 -0
  282. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_run_until_records_in_org__one_needed.yaml +24 -0
  283. cumulusci/tasks/bulkdata/tests/cassettes/TestSnowfakery.test_snowfakery_query_salesforce.yaml +25 -0
  284. cumulusci/tasks/bulkdata/tests/cassettes/TestUpdatesIntegrationTests.test_updates_task.yaml +80 -0
  285. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_simple_upsert__rest.yaml +270 -0
  286. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert__rest.yaml +267 -0
  287. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field__rest.yaml +369 -0
  288. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_external_id_field_rest__duplicate_error.yaml +204 -0
  289. cumulusci/tasks/bulkdata/tests/cassettes/TestUpsert.test_upsert_complex_fields__bulk.yaml +675 -0
  290. cumulusci/tasks/bulkdata/tests/dummy_data_factory.py +36 -0
  291. cumulusci/tasks/bulkdata/tests/integration_test_utils.py +49 -0
  292. cumulusci/tasks/bulkdata/tests/mapping-oid.yml +87 -0
  293. cumulusci/tasks/bulkdata/tests/mapping_after.yml +38 -0
  294. cumulusci/tasks/bulkdata/tests/mapping_poly.yml +34 -0
  295. cumulusci/tasks/bulkdata/tests/mapping_poly_incomplete.yml +20 -0
  296. cumulusci/tasks/bulkdata/tests/mapping_poly_wrong.yml +21 -0
  297. cumulusci/tasks/bulkdata/tests/mapping_select.yml +20 -0
  298. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_strategy.yml +20 -0
  299. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_number.yml +21 -0
  300. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__invalid_strategy.yml +21 -0
  301. cumulusci/tasks/bulkdata/tests/mapping_select_invalid_threshold__non_float.yml +21 -0
  302. cumulusci/tasks/bulkdata/tests/mapping_select_missing_priority_fields.yml +22 -0
  303. cumulusci/tasks/bulkdata/tests/mapping_select_no_priority_fields.yml +18 -0
  304. cumulusci/tasks/bulkdata/tests/mapping_simple.yml +27 -0
  305. cumulusci/tasks/bulkdata/tests/mapping_v1.yml +28 -0
  306. cumulusci/tasks/bulkdata/tests/mapping_v2.yml +21 -0
  307. cumulusci/tasks/bulkdata/tests/mapping_v3.yml +32 -0
  308. cumulusci/tasks/bulkdata/tests/mapping_vanilla_sf.yml +69 -0
  309. cumulusci/tasks/bulkdata/tests/mock_data_factory_without_mapping.py +12 -0
  310. cumulusci/tasks/bulkdata/tests/person_accounts.yml +23 -0
  311. cumulusci/tasks/bulkdata/tests/person_accounts_minimal.yml +15 -0
  312. cumulusci/tasks/bulkdata/tests/recordtypes.yml +8 -0
  313. cumulusci/tasks/bulkdata/tests/recordtypes_2.yml +6 -0
  314. cumulusci/tasks/bulkdata/tests/recordtypes_with_ispersontype.yml +8 -0
  315. cumulusci/tasks/bulkdata/tests/snowfakery/child/child2.yml +3 -0
  316. cumulusci/tasks/bulkdata/tests/snowfakery/child.yml +4 -0
  317. cumulusci/tasks/bulkdata/tests/snowfakery/gen_npsp_standard_objects.recipe.yml +89 -0
  318. cumulusci/tasks/bulkdata/tests/snowfakery/include_parent.yml +3 -0
  319. cumulusci/tasks/bulkdata/tests/snowfakery/npsp_standard_objects_macros.yml +34 -0
  320. cumulusci/tasks/bulkdata/tests/snowfakery/options.recipe.yml +6 -0
  321. cumulusci/tasks/bulkdata/tests/snowfakery/query_snowfakery.recipe.yml +16 -0
  322. cumulusci/tasks/bulkdata/tests/snowfakery/sf_standard_object_macros.yml +83 -0
  323. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.load.yml +2 -0
  324. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery.recipe.yml +13 -0
  325. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_2.load.yml +5 -0
  326. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.load.yml +13 -0
  327. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels.recipe.yml +12 -0
  328. cumulusci/tasks/bulkdata/tests/snowfakery/simple_snowfakery_channels_2.load.yml +13 -0
  329. cumulusci/tasks/bulkdata/tests/snowfakery/unique_values.recipe.yml +4 -0
  330. cumulusci/tasks/bulkdata/tests/snowfakery/upsert.recipe.yml +23 -0
  331. cumulusci/tasks/bulkdata/tests/snowfakery/upsert_2.recipe.yml +29 -0
  332. cumulusci/tasks/bulkdata/tests/snowfakery/upsert_before.yml +10 -0
  333. cumulusci/tasks/bulkdata/tests/test_base_generate_data_tasks.py +61 -0
  334. cumulusci/tasks/bulkdata/tests/test_dates.py +99 -0
  335. cumulusci/tasks/bulkdata/tests/test_delete.py +404 -0
  336. cumulusci/tasks/bulkdata/tests/test_extract.py +1311 -0
  337. cumulusci/tasks/bulkdata/tests/test_factory_utils.py +55 -0
  338. cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +252 -0
  339. cumulusci/tasks/bulkdata/tests/test_generate_from_snowfakery_task.py +343 -0
  340. cumulusci/tasks/bulkdata/tests/test_generatemapping.py +1039 -0
  341. cumulusci/tasks/bulkdata/tests/test_load.py +3175 -0
  342. cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +1658 -0
  343. cumulusci/tasks/bulkdata/tests/test_query_db__joins_self_lookups.yml +12 -0
  344. cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups.yml +26 -0
  345. cumulusci/tasks/bulkdata/tests/test_query_db_joins_lookups_select.yml +48 -0
  346. cumulusci/tasks/bulkdata/tests/test_select.py +171 -0
  347. cumulusci/tasks/bulkdata/tests/test_select_utils.py +1057 -0
  348. cumulusci/tasks/bulkdata/tests/test_snowfakery.py +1153 -0
  349. cumulusci/tasks/bulkdata/tests/test_step.py +3957 -0
  350. cumulusci/tasks/bulkdata/tests/test_updates.py +513 -0
  351. cumulusci/tasks/bulkdata/tests/test_upsert.py +1015 -0
  352. cumulusci/tasks/bulkdata/tests/test_utils.py +158 -0
  353. cumulusci/tasks/bulkdata/tests/testdata.db +0 -0
  354. cumulusci/tasks/bulkdata/tests/update_describe.py +50 -0
  355. cumulusci/tasks/bulkdata/tests/update_person_accounts.yml +23 -0
  356. cumulusci/tasks/bulkdata/tests/utils.py +114 -0
  357. cumulusci/tasks/bulkdata/update_data.py +260 -0
  358. cumulusci/tasks/bulkdata/upsert_utils.py +130 -0
  359. cumulusci/tasks/bulkdata/utils.py +249 -0
  360. cumulusci/tasks/command.py +178 -0
  361. cumulusci/tasks/connectedapp.py +186 -0
  362. cumulusci/tasks/create_package_version.py +778 -0
  363. cumulusci/tasks/datadictionary.py +745 -0
  364. cumulusci/tasks/dx_convert_from.py +26 -0
  365. cumulusci/tasks/github/__init__.py +17 -0
  366. cumulusci/tasks/github/base.py +16 -0
  367. cumulusci/tasks/github/commit_status.py +13 -0
  368. cumulusci/tasks/github/merge.py +11 -0
  369. cumulusci/tasks/github/publish.py +11 -0
  370. cumulusci/tasks/github/pull_request.py +11 -0
  371. cumulusci/tasks/github/release.py +11 -0
  372. cumulusci/tasks/github/release_report.py +11 -0
  373. cumulusci/tasks/github/tag.py +11 -0
  374. cumulusci/tasks/github/tests/__init__.py +0 -0
  375. cumulusci/tasks/github/tests/test_util.py +202 -0
  376. cumulusci/tasks/github/tests/test_vcs_migration.py +44 -0
  377. cumulusci/tasks/github/tests/util_github_api.py +666 -0
  378. cumulusci/tasks/github/util.py +252 -0
  379. cumulusci/tasks/marketing_cloud/__init__.py +0 -0
  380. cumulusci/tasks/marketing_cloud/api.py +188 -0
  381. cumulusci/tasks/marketing_cloud/base.py +38 -0
  382. cumulusci/tasks/marketing_cloud/deploy.py +345 -0
  383. cumulusci/tasks/marketing_cloud/get_user_info.py +40 -0
  384. cumulusci/tasks/marketing_cloud/mc_constants.py +1 -0
  385. cumulusci/tasks/marketing_cloud/tests/__init__.py +0 -0
  386. cumulusci/tasks/marketing_cloud/tests/conftest.py +46 -0
  387. cumulusci/tasks/marketing_cloud/tests/expected-payload.json +110 -0
  388. cumulusci/tasks/marketing_cloud/tests/test_api.py +97 -0
  389. cumulusci/tasks/marketing_cloud/tests/test_api_soap_envelopes.py +145 -0
  390. cumulusci/tasks/marketing_cloud/tests/test_base.py +14 -0
  391. cumulusci/tasks/marketing_cloud/tests/test_deploy.py +400 -0
  392. cumulusci/tasks/marketing_cloud/tests/test_get_user_info.py +141 -0
  393. cumulusci/tasks/marketing_cloud/tests/validation-response.json +39 -0
  394. cumulusci/tasks/metadata/__init__.py +0 -0
  395. cumulusci/tasks/metadata/ee_src.py +94 -0
  396. cumulusci/tasks/metadata/managed_src.py +100 -0
  397. cumulusci/tasks/metadata/metadata_map.yml +868 -0
  398. cumulusci/tasks/metadata/modify.py +99 -0
  399. cumulusci/tasks/metadata/package.py +684 -0
  400. cumulusci/tasks/metadata/tests/__init__.py +0 -0
  401. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/.hidden/.keep +0 -0
  402. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/destructiveChanges.xml +9 -0
  403. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package.xml +9 -0
  404. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/package_install_uninstall.xml +11 -0
  405. cumulusci/tasks/metadata/tests/package_metadata/namespaced_report_folder/reports/namespace__TestFolder/TestReport.report +3 -0
  406. cumulusci/tasks/metadata/tests/sample_package.xml +9 -0
  407. cumulusci/tasks/metadata/tests/test_ee_src.py +112 -0
  408. cumulusci/tasks/metadata/tests/test_managed_src.py +111 -0
  409. cumulusci/tasks/metadata/tests/test_modify.py +123 -0
  410. cumulusci/tasks/metadata/tests/test_package.py +476 -0
  411. cumulusci/tasks/metadata_etl/__init__.py +29 -0
  412. cumulusci/tasks/metadata_etl/base.py +436 -0
  413. cumulusci/tasks/metadata_etl/duplicate_rules.py +24 -0
  414. cumulusci/tasks/metadata_etl/field_sets.py +70 -0
  415. cumulusci/tasks/metadata_etl/help_text.py +92 -0
  416. cumulusci/tasks/metadata_etl/layouts.py +550 -0
  417. cumulusci/tasks/metadata_etl/objects.py +68 -0
  418. cumulusci/tasks/metadata_etl/permissions.py +167 -0
  419. cumulusci/tasks/metadata_etl/picklists.py +221 -0
  420. cumulusci/tasks/metadata_etl/remote_site_settings.py +99 -0
  421. cumulusci/tasks/metadata_etl/sharing.py +138 -0
  422. cumulusci/tasks/metadata_etl/tests/test_base.py +512 -0
  423. cumulusci/tasks/metadata_etl/tests/test_duplicate_rules.py +22 -0
  424. cumulusci/tasks/metadata_etl/tests/test_field_sets.py +156 -0
  425. cumulusci/tasks/metadata_etl/tests/test_help_text.py +387 -0
  426. cumulusci/tasks/metadata_etl/tests/test_ip_ranges.py +85 -0
  427. cumulusci/tasks/metadata_etl/tests/test_layouts.py +858 -0
  428. cumulusci/tasks/metadata_etl/tests/test_objects.py +236 -0
  429. cumulusci/tasks/metadata_etl/tests/test_permissions.py +223 -0
  430. cumulusci/tasks/metadata_etl/tests/test_picklists.py +547 -0
  431. cumulusci/tasks/metadata_etl/tests/test_remote_site_settings.py +46 -0
  432. cumulusci/tasks/metadata_etl/tests/test_sharing.py +333 -0
  433. cumulusci/tasks/metadata_etl/tests/test_value_sets.py +298 -0
  434. cumulusci/tasks/metadata_etl/value_sets.py +106 -0
  435. cumulusci/tasks/metadeploy.py +393 -0
  436. cumulusci/tasks/metaxml.py +88 -0
  437. cumulusci/tasks/preflight/__init__.py +0 -0
  438. cumulusci/tasks/preflight/dataset_load.py +49 -0
  439. cumulusci/tasks/preflight/licenses.py +86 -0
  440. cumulusci/tasks/preflight/packages.py +14 -0
  441. cumulusci/tasks/preflight/permsets.py +23 -0
  442. cumulusci/tasks/preflight/recordtypes.py +16 -0
  443. cumulusci/tasks/preflight/retrieve_tasks.py +30 -0
  444. cumulusci/tasks/preflight/settings.py +77 -0
  445. cumulusci/tasks/preflight/sobjects.py +202 -0
  446. cumulusci/tasks/preflight/tests/test_dataset_load.py +85 -0
  447. cumulusci/tasks/preflight/tests/test_licenses.py +174 -0
  448. cumulusci/tasks/preflight/tests/test_packages.py +14 -0
  449. cumulusci/tasks/preflight/tests/test_permset_preflights.py +51 -0
  450. cumulusci/tasks/preflight/tests/test_recordtypes.py +30 -0
  451. cumulusci/tasks/preflight/tests/test_retrieve_tasks.py +62 -0
  452. cumulusci/tasks/preflight/tests/test_settings.py +130 -0
  453. cumulusci/tasks/preflight/tests/test_sobjects.py +231 -0
  454. cumulusci/tasks/push/README.md +59 -0
  455. cumulusci/tasks/push/__init__.py +0 -0
  456. cumulusci/tasks/push/push_api.py +659 -0
  457. cumulusci/tasks/push/pushfails.py +136 -0
  458. cumulusci/tasks/push/tasks.py +476 -0
  459. cumulusci/tasks/push/tests/conftest.py +263 -0
  460. cumulusci/tasks/push/tests/test_push_api.py +951 -0
  461. cumulusci/tasks/push/tests/test_push_tasks.py +659 -0
  462. cumulusci/tasks/release_notes/README.md +63 -0
  463. cumulusci/tasks/release_notes/__init__.py +0 -0
  464. cumulusci/tasks/release_notes/exceptions.py +5 -0
  465. cumulusci/tasks/release_notes/generator.py +137 -0
  466. cumulusci/tasks/release_notes/parser.py +232 -0
  467. cumulusci/tasks/release_notes/provider.py +44 -0
  468. cumulusci/tasks/release_notes/task.py +300 -0
  469. cumulusci/tasks/release_notes/tests/__init__.py +0 -0
  470. cumulusci/tasks/release_notes/tests/change_notes/full/example1.md +17 -0
  471. cumulusci/tasks/release_notes/tests/change_notes/multi/1.txt +1 -0
  472. cumulusci/tasks/release_notes/tests/change_notes/multi/2.txt +1 -0
  473. cumulusci/tasks/release_notes/tests/change_notes/multi/3.txt +1 -0
  474. cumulusci/tasks/release_notes/tests/change_notes/single/1.txt +1 -0
  475. cumulusci/tasks/release_notes/tests/test_generator.py +582 -0
  476. cumulusci/tasks/release_notes/tests/test_parser.py +867 -0
  477. cumulusci/tasks/release_notes/tests/test_provider.py +512 -0
  478. cumulusci/tasks/release_notes/tests/test_task.py +461 -0
  479. cumulusci/tasks/release_notes/tests/utils.py +153 -0
  480. cumulusci/tasks/robotframework/__init__.py +3 -0
  481. cumulusci/tasks/robotframework/debugger/DebugListener.py +100 -0
  482. cumulusci/tasks/robotframework/debugger/__init__.py +10 -0
  483. cumulusci/tasks/robotframework/debugger/model.py +87 -0
  484. cumulusci/tasks/robotframework/debugger/ui.py +259 -0
  485. cumulusci/tasks/robotframework/libdoc.py +269 -0
  486. cumulusci/tasks/robotframework/robotframework.py +392 -0
  487. cumulusci/tasks/robotframework/stylesheet.css +130 -0
  488. cumulusci/tasks/robotframework/template.html +109 -0
  489. cumulusci/tasks/robotframework/tests/TestLibrary.py +18 -0
  490. cumulusci/tasks/robotframework/tests/TestPageObjects.py +31 -0
  491. cumulusci/tasks/robotframework/tests/TestResource.robot +8 -0
  492. cumulusci/tasks/robotframework/tests/failing_tests.robot +16 -0
  493. cumulusci/tasks/robotframework/tests/performance.robot +23 -0
  494. cumulusci/tasks/robotframework/tests/test_browser_proxies.py +137 -0
  495. cumulusci/tasks/robotframework/tests/test_debugger.py +360 -0
  496. cumulusci/tasks/robotframework/tests/test_robot_parallel.py +141 -0
  497. cumulusci/tasks/robotframework/tests/test_robotframework.py +860 -0
  498. cumulusci/tasks/salesforce/BaseRetrieveMetadata.py +58 -0
  499. cumulusci/tasks/salesforce/BaseSalesforceApiTask.py +45 -0
  500. cumulusci/tasks/salesforce/BaseSalesforceMetadataApiTask.py +18 -0
  501. cumulusci/tasks/salesforce/BaseSalesforceTask.py +4 -0
  502. cumulusci/tasks/salesforce/BaseUninstallMetadata.py +41 -0
  503. cumulusci/tasks/salesforce/CreateCommunity.py +124 -0
  504. cumulusci/tasks/salesforce/CreatePackage.py +29 -0
  505. cumulusci/tasks/salesforce/Deploy.py +240 -0
  506. cumulusci/tasks/salesforce/DeployBundles.py +88 -0
  507. cumulusci/tasks/salesforce/DescribeMetadataTypes.py +26 -0
  508. cumulusci/tasks/salesforce/EnsureRecordTypes.py +202 -0
  509. cumulusci/tasks/salesforce/GetInstalledPackages.py +8 -0
  510. cumulusci/tasks/salesforce/ListCommunities.py +40 -0
  511. cumulusci/tasks/salesforce/ListCommunityTemplates.py +19 -0
  512. cumulusci/tasks/salesforce/PublishCommunity.py +62 -0
  513. cumulusci/tasks/salesforce/RetrievePackaged.py +41 -0
  514. cumulusci/tasks/salesforce/RetrieveReportsAndDashboards.py +82 -0
  515. cumulusci/tasks/salesforce/RetrieveUnpackaged.py +36 -0
  516. cumulusci/tasks/salesforce/SOQLQuery.py +39 -0
  517. cumulusci/tasks/salesforce/UninstallLocal.py +15 -0
  518. cumulusci/tasks/salesforce/UninstallLocalBundles.py +28 -0
  519. cumulusci/tasks/salesforce/UninstallLocalNamespacedBundles.py +58 -0
  520. cumulusci/tasks/salesforce/UninstallPackage.py +32 -0
  521. cumulusci/tasks/salesforce/UninstallPackaged.py +56 -0
  522. cumulusci/tasks/salesforce/UpdateAdminProfile.py +8 -0
  523. cumulusci/tasks/salesforce/__init__.py +79 -0
  524. cumulusci/tasks/salesforce/activate_flow.py +74 -0
  525. cumulusci/tasks/salesforce/check_components.py +324 -0
  526. cumulusci/tasks/salesforce/composite.py +142 -0
  527. cumulusci/tasks/salesforce/create_permission_sets.py +35 -0
  528. cumulusci/tasks/salesforce/custom_settings.py +134 -0
  529. cumulusci/tasks/salesforce/custom_settings_wait.py +132 -0
  530. cumulusci/tasks/salesforce/enable_prediction.py +107 -0
  531. cumulusci/tasks/salesforce/insert_record.py +40 -0
  532. cumulusci/tasks/salesforce/install_package_version.py +242 -0
  533. cumulusci/tasks/salesforce/license_preflights.py +8 -0
  534. cumulusci/tasks/salesforce/network_member_group.py +178 -0
  535. cumulusci/tasks/salesforce/nonsourcetracking.py +228 -0
  536. cumulusci/tasks/salesforce/org_settings.py +193 -0
  537. cumulusci/tasks/salesforce/package_upload.py +328 -0
  538. cumulusci/tasks/salesforce/profiles.py +74 -0
  539. cumulusci/tasks/salesforce/promote_package_version.py +376 -0
  540. cumulusci/tasks/salesforce/retrieve_profile.py +195 -0
  541. cumulusci/tasks/salesforce/salesforce_files.py +244 -0
  542. cumulusci/tasks/salesforce/sourcetracking.py +507 -0
  543. cumulusci/tasks/salesforce/tests/__init__.py +3 -0
  544. cumulusci/tasks/salesforce/tests/test_CreateCommunity.py +278 -0
  545. cumulusci/tasks/salesforce/tests/test_CreatePackage.py +22 -0
  546. cumulusci/tasks/salesforce/tests/test_Deploy.py +470 -0
  547. cumulusci/tasks/salesforce/tests/test_DeployBundles.py +76 -0
  548. cumulusci/tasks/salesforce/tests/test_EnsureRecordTypes.py +345 -0
  549. cumulusci/tasks/salesforce/tests/test_ListCommunities.py +84 -0
  550. cumulusci/tasks/salesforce/tests/test_ListCommunityTemplates.py +49 -0
  551. cumulusci/tasks/salesforce/tests/test_PackageUpload.py +547 -0
  552. cumulusci/tasks/salesforce/tests/test_ProfileGrantAllAccess.py +699 -0
  553. cumulusci/tasks/salesforce/tests/test_PublishCommunity.py +181 -0
  554. cumulusci/tasks/salesforce/tests/test_RetrievePackaged.py +24 -0
  555. cumulusci/tasks/salesforce/tests/test_RetrieveReportsAndDashboards.py +56 -0
  556. cumulusci/tasks/salesforce/tests/test_RetrieveUnpackaged.py +21 -0
  557. cumulusci/tasks/salesforce/tests/test_SOQLQuery.py +30 -0
  558. cumulusci/tasks/salesforce/tests/test_UninstallLocal.py +15 -0
  559. cumulusci/tasks/salesforce/tests/test_UninstallLocalBundles.py +19 -0
  560. cumulusci/tasks/salesforce/tests/test_UninstallLocalNamespacedBundles.py +22 -0
  561. cumulusci/tasks/salesforce/tests/test_UninstallPackage.py +19 -0
  562. cumulusci/tasks/salesforce/tests/test_UninstallPackaged.py +66 -0
  563. cumulusci/tasks/salesforce/tests/test_UninstallPackagedIncremental.py +127 -0
  564. cumulusci/tasks/salesforce/tests/test_activate_flow.py +132 -0
  565. cumulusci/tasks/salesforce/tests/test_base_tasks.py +110 -0
  566. cumulusci/tasks/salesforce/tests/test_check_components.py +445 -0
  567. cumulusci/tasks/salesforce/tests/test_composite.py +250 -0
  568. cumulusci/tasks/salesforce/tests/test_create_permission_sets.py +41 -0
  569. cumulusci/tasks/salesforce/tests/test_custom_settings.py +227 -0
  570. cumulusci/tasks/salesforce/tests/test_custom_settings_wait.py +174 -0
  571. cumulusci/tasks/salesforce/tests/test_describemetadatatypes.py +18 -0
  572. cumulusci/tasks/salesforce/tests/test_enable_prediction.py +240 -0
  573. cumulusci/tasks/salesforce/tests/test_insert_record.py +110 -0
  574. cumulusci/tasks/salesforce/tests/test_install_package_version.py +464 -0
  575. cumulusci/tasks/salesforce/tests/test_network_member_group.py +444 -0
  576. cumulusci/tasks/salesforce/tests/test_nonsourcetracking.py +235 -0
  577. cumulusci/tasks/salesforce/tests/test_org_settings.py +407 -0
  578. cumulusci/tasks/salesforce/tests/test_profiles.py +202 -0
  579. cumulusci/tasks/salesforce/tests/test_retrieve_profile.py +287 -0
  580. cumulusci/tasks/salesforce/tests/test_salesforce_files.py +228 -0
  581. cumulusci/tasks/salesforce/tests/test_sourcetracking.py +350 -0
  582. cumulusci/tasks/salesforce/tests/test_trigger_handlers.py +300 -0
  583. cumulusci/tasks/salesforce/tests/test_update_dependencies.py +509 -0
  584. cumulusci/tasks/salesforce/tests/util.py +79 -0
  585. cumulusci/tasks/salesforce/trigger_handlers.py +119 -0
  586. cumulusci/tasks/salesforce/uninstall_packaged_incremental.py +136 -0
  587. cumulusci/tasks/salesforce/update_dependencies.py +290 -0
  588. cumulusci/tasks/salesforce/update_profile.py +339 -0
  589. cumulusci/tasks/salesforce/users/permsets.py +227 -0
  590. cumulusci/tasks/salesforce/users/photos.py +162 -0
  591. cumulusci/tasks/salesforce/users/tests/photo.mock.txt +1 -0
  592. cumulusci/tasks/salesforce/users/tests/test_permsets.py +950 -0
  593. cumulusci/tasks/salesforce/users/tests/test_photos.py +373 -0
  594. cumulusci/tasks/sample_data/capture_sample_data.py +77 -0
  595. cumulusci/tasks/sample_data/load_sample_data.py +85 -0
  596. cumulusci/tasks/sample_data/test_capture_sample_data.py +117 -0
  597. cumulusci/tasks/sample_data/test_load_sample_data.py +121 -0
  598. cumulusci/tasks/sfdx.py +83 -0
  599. cumulusci/tasks/tests/__init__.py +1 -0
  600. cumulusci/tasks/tests/conftest.py +30 -0
  601. cumulusci/tasks/tests/test_command.py +129 -0
  602. cumulusci/tasks/tests/test_connectedapp.py +236 -0
  603. cumulusci/tasks/tests/test_create_package_version.py +847 -0
  604. cumulusci/tasks/tests/test_datadictionary.py +1575 -0
  605. cumulusci/tasks/tests/test_dx_convert_from.py +60 -0
  606. cumulusci/tasks/tests/test_metadeploy.py +624 -0
  607. cumulusci/tasks/tests/test_metaxml.py +99 -0
  608. cumulusci/tasks/tests/test_promote_package_version.py +488 -0
  609. cumulusci/tasks/tests/test_pushfails.py +96 -0
  610. cumulusci/tasks/tests/test_salesforce.py +72 -0
  611. cumulusci/tasks/tests/test_sfdx.py +105 -0
  612. cumulusci/tasks/tests/test_util.py +207 -0
  613. cumulusci/tasks/util.py +261 -0
  614. cumulusci/tasks/vcs/__init__.py +19 -0
  615. cumulusci/tasks/vcs/commit_status.py +58 -0
  616. cumulusci/tasks/vcs/create_commit_status.py +37 -0
  617. cumulusci/tasks/vcs/download_extract.py +199 -0
  618. cumulusci/tasks/vcs/merge.py +298 -0
  619. cumulusci/tasks/vcs/publish.py +207 -0
  620. cumulusci/tasks/vcs/pull_request.py +9 -0
  621. cumulusci/tasks/vcs/release.py +134 -0
  622. cumulusci/tasks/vcs/release_report.py +105 -0
  623. cumulusci/tasks/vcs/tag.py +31 -0
  624. cumulusci/tasks/vcs/tests/github/test_commit_status.py +196 -0
  625. cumulusci/tasks/vcs/tests/github/test_download_extract.py +896 -0
  626. cumulusci/tasks/vcs/tests/github/test_merge.py +1118 -0
  627. cumulusci/tasks/vcs/tests/github/test_publish.py +823 -0
  628. cumulusci/tasks/vcs/tests/github/test_pull_request.py +29 -0
  629. cumulusci/tasks/vcs/tests/github/test_release.py +390 -0
  630. cumulusci/tasks/vcs/tests/github/test_release_report.py +109 -0
  631. cumulusci/tasks/vcs/tests/github/test_tag.py +90 -0
  632. cumulusci/tasks/vlocity/exceptions.py +2 -0
  633. cumulusci/tasks/vlocity/tests/test_vlocity.py +283 -0
  634. cumulusci/tasks/vlocity/vlocity.py +342 -0
  635. cumulusci/tests/__init__.py +1 -0
  636. cumulusci/tests/cassettes/GET_sobjects_Account_PersonAccount_describe.yaml +18 -0
  637. cumulusci/tests/cassettes/TestIntegrationInfrastructure.test_integration_tests.yaml +19 -0
  638. cumulusci/tests/pytest_plugins/pytest_sf_orgconnect.py +307 -0
  639. cumulusci/tests/pytest_plugins/pytest_sf_vcr.py +275 -0
  640. cumulusci/tests/pytest_plugins/pytest_sf_vcr_serializer.py +160 -0
  641. cumulusci/tests/pytest_plugins/pytest_typeguard.py +5 -0
  642. cumulusci/tests/pytest_plugins/test_vcr_string_compressor.py +49 -0
  643. cumulusci/tests/pytest_plugins/vcr_string_compressor.py +97 -0
  644. cumulusci/tests/shared_cassettes/GET_sobjects_Account_describe.yaml +18 -0
  645. cumulusci/tests/shared_cassettes/GET_sobjects_Case_describe.yaml +18 -0
  646. cumulusci/tests/shared_cassettes/GET_sobjects_Contact_describe.yaml +4838 -0
  647. cumulusci/tests/shared_cassettes/GET_sobjects_Custom__c_describe.yaml +242 -0
  648. cumulusci/tests/shared_cassettes/GET_sobjects_Event_describe.yaml +19 -0
  649. cumulusci/tests/shared_cassettes/GET_sobjects_Global_describe.yaml +1338 -0
  650. cumulusci/tests/shared_cassettes/GET_sobjects_Lead_describe.yaml +18 -0
  651. cumulusci/tests/shared_cassettes/GET_sobjects_OpportunityContactRole_describe.yaml +34 -0
  652. cumulusci/tests/shared_cassettes/GET_sobjects_Opportunity_describe.yaml +1261 -0
  653. cumulusci/tests/shared_cassettes/GET_sobjects_Organization.yaml +49 -0
  654. cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfoList_xml.tpl +15 -0
  655. cumulusci/tests/shared_cassettes/vcr_string_templates/batchInfo_xml.tpl +13 -0
  656. cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_insert_xml.tpl +24 -0
  657. cumulusci/tests/shared_cassettes/vcr_string_templates/jobInfo_upsert_xml.tpl +25 -0
  658. cumulusci/tests/test_entry_points.py +20 -0
  659. cumulusci/tests/test_integration_infrastructure.py +131 -0
  660. cumulusci/tests/test_main.py +9 -0
  661. cumulusci/tests/test_schema.py +32 -0
  662. cumulusci/tests/test_utils.py +657 -0
  663. cumulusci/tests/test_vcr_serializer.py +134 -0
  664. cumulusci/tests/uncompressed_cassette.yaml +83 -0
  665. cumulusci/tests/util.py +344 -0
  666. cumulusci/utils/__init__.py +731 -0
  667. cumulusci/utils/classutils.py +9 -0
  668. cumulusci/utils/collections.py +32 -0
  669. cumulusci/utils/deprecation.py +11 -0
  670. cumulusci/utils/encryption.py +31 -0
  671. cumulusci/utils/fileutils.py +295 -0
  672. cumulusci/utils/git.py +142 -0
  673. cumulusci/utils/http/multi_request.py +214 -0
  674. cumulusci/utils/http/requests_utils.py +103 -0
  675. cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +32 -0
  676. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_composite_parallel_salesforce.yaml +65 -0
  677. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_errors.yaml +24 -0
  678. cumulusci/utils/http/tests/cassettes/TestCompositeParallelSalesforce.test_reference_ids.yaml +49 -0
  679. cumulusci/utils/http/tests/test_multi_request.py +255 -0
  680. cumulusci/utils/iterators.py +21 -0
  681. cumulusci/utils/logging.py +128 -0
  682. cumulusci/utils/metaprogramming.py +10 -0
  683. cumulusci/utils/options.py +138 -0
  684. cumulusci/utils/parallel/queries_in_parallel/run_queries_in_parallel.py +29 -0
  685. cumulusci/utils/parallel/queries_in_parallel/tests/test_run_queries_in_parallel.py +50 -0
  686. cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +238 -0
  687. cumulusci/utils/parallel/task_worker_queues/parallel_worker_queue.py +243 -0
  688. cumulusci/utils/parallel/task_worker_queues/tests/test_parallel_worker.py +353 -0
  689. cumulusci/utils/salesforce/count_sobjects.py +46 -0
  690. cumulusci/utils/salesforce/soql.py +17 -0
  691. cumulusci/utils/salesforce/tests/cassettes/ManualEdit_TestCountSObjects.test_count_sobjects__network_errors.yaml +23 -0
  692. cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects__errors.yaml +33 -0
  693. cumulusci/utils/salesforce/tests/cassettes/TestCountSObjects.test_count_sobjects_simple.yaml +29 -0
  694. cumulusci/utils/salesforce/tests/test_count_sobjects.py +29 -0
  695. cumulusci/utils/salesforce/tests/test_soql.py +30 -0
  696. cumulusci/utils/tests/cassettes/ManualEditTestDescribeOrg.test_minimal_schema.yaml +36 -0
  697. cumulusci/utils/tests/cassettes/ManualEdit_test_describe_to_sql.yaml +191 -0
  698. cumulusci/utils/tests/test_fileutils.py +284 -0
  699. cumulusci/utils/tests/test_git.py +85 -0
  700. cumulusci/utils/tests/test_logging.py +70 -0
  701. cumulusci/utils/tests/test_option_parsing.py +188 -0
  702. cumulusci/utils/tests/test_org_schema.py +691 -0
  703. cumulusci/utils/tests/test_org_schema_models.py +79 -0
  704. cumulusci/utils/tests/test_waiting.py +25 -0
  705. cumulusci/utils/version_strings.py +391 -0
  706. cumulusci/utils/waiting.py +42 -0
  707. cumulusci/utils/xml/__init__.py +91 -0
  708. cumulusci/utils/xml/metadata_tree.py +299 -0
  709. cumulusci/utils/xml/robot_xml.py +114 -0
  710. cumulusci/utils/xml/salesforce_encoding.py +100 -0
  711. cumulusci/utils/xml/test/test_metadata_tree.py +251 -0
  712. cumulusci/utils/xml/test/test_salesforce_encoding.py +173 -0
  713. cumulusci/utils/yaml/cumulusci_yml.py +401 -0
  714. cumulusci/utils/yaml/model_parser.py +156 -0
  715. cumulusci/utils/yaml/safer_loader.py +74 -0
  716. cumulusci/utils/yaml/tests/bad_cci.yml +5 -0
  717. cumulusci/utils/yaml/tests/cassettes/TestCumulusciYml.test_validate_url__with_errors.yaml +20 -0
  718. cumulusci/utils/yaml/tests/test_cumulusci_yml.py +286 -0
  719. cumulusci/utils/yaml/tests/test_model_parser.py +175 -0
  720. cumulusci/utils/yaml/tests/test_safer_loader.py +88 -0
  721. cumulusci/utils/ziputils.py +61 -0
  722. cumulusci/vcs/base.py +143 -0
  723. cumulusci/vcs/bootstrap.py +272 -0
  724. cumulusci/vcs/github/__init__.py +24 -0
  725. cumulusci/vcs/github/adapter.py +689 -0
  726. cumulusci/vcs/github/release_notes/generator.py +219 -0
  727. cumulusci/vcs/github/release_notes/parser.py +151 -0
  728. cumulusci/vcs/github/release_notes/provider.py +143 -0
  729. cumulusci/vcs/github/service.py +569 -0
  730. cumulusci/vcs/github/tests/test_adapter.py +138 -0
  731. cumulusci/vcs/github/tests/test_service.py +408 -0
  732. cumulusci/vcs/models.py +586 -0
  733. cumulusci/vcs/tests/conftest.py +41 -0
  734. cumulusci/vcs/tests/dummy_service.py +241 -0
  735. cumulusci/vcs/tests/test_vcs_base.py +687 -0
  736. cumulusci/vcs/tests/test_vcs_bootstrap.py +727 -0
  737. cumulusci/vcs/utils/__init__.py +31 -0
  738. cumulusci/vcs/vcs_source.py +287 -0
  739. cumulusci_plus-5.0.0.dist-info/METADATA +145 -0
  740. cumulusci_plus-5.0.0.dist-info/RECORD +744 -0
  741. cumulusci_plus-5.0.0.dist-info/WHEEL +4 -0
  742. cumulusci_plus-5.0.0.dist-info/entry_points.txt +3 -0
  743. cumulusci_plus-5.0.0.dist-info/licenses/AUTHORS.rst +41 -0
  744. 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