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,3957 @@
1
+ import io
2
+ import json
3
+ from itertools import tee
4
+ from unittest import mock
5
+
6
+ import pytest
7
+ import responses
8
+
9
+ from cumulusci.core.exceptions import BulkDataException
10
+ from cumulusci.tasks.bulkdata.load import LoadData
11
+ from cumulusci.tasks.bulkdata.select_utils import SelectStrategy
12
+ from cumulusci.tasks.bulkdata.step import (
13
+ HIGH_PRIORITY_VALUE,
14
+ LOW_PRIORITY_VALUE,
15
+ BulkApiDmlOperation,
16
+ BulkApiQueryOperation,
17
+ BulkJobMixin,
18
+ DataApi,
19
+ DataOperationJobResult,
20
+ DataOperationResult,
21
+ DataOperationStatus,
22
+ DataOperationType,
23
+ RestApiDmlOperation,
24
+ RestApiQueryOperation,
25
+ assign_weights,
26
+ download_file,
27
+ extract_flattened_headers,
28
+ flatten_record,
29
+ get_dml_operation,
30
+ get_query_operation,
31
+ )
32
+ from cumulusci.tasks.bulkdata.tests.utils import _make_task
33
+ from cumulusci.tests.util import CURRENT_SF_API_VERSION, mock_describe_calls
34
+
35
+ BULK_BATCH_RESPONSE = """<root xmlns="http://ns">
36
+ <batch>
37
+ <state>{first_state}</state>
38
+ <stateMessage>{first_message}</stateMessage>
39
+ </batch>
40
+ <batch>
41
+ <state>{second_state}</state>
42
+ <stateMessage>{second_message}</stateMessage>
43
+ </batch>
44
+ </root>"""
45
+
46
+
47
+ class TestDownloadFile:
48
+ @responses.activate
49
+ def test_download_file(self):
50
+ url = "https://example.com"
51
+ bulk_mock = mock.Mock()
52
+ bulk_mock.headers.return_value = {}
53
+
54
+ responses.add(method="GET", url=url, body=b"TEST\xe2\x80\x94")
55
+ with download_file(url, bulk_mock) as f:
56
+ # make sure it was decoded as utf-8
57
+ assert f.read() == "TEST\u2014"
58
+
59
+
60
+ class TestBulkDataJobTaskMixin:
61
+ @responses.activate
62
+ def test_job_state_from_batches(self):
63
+ mixin = BulkJobMixin()
64
+ mixin.bulk = mock.Mock()
65
+ mixin.bulk.endpoint = "https://example.com"
66
+ mixin.bulk.headers.return_value = {"HEADER": "test"}
67
+ mixin._parse_job_state = mock.Mock()
68
+
69
+ responses.add(
70
+ "GET",
71
+ "https://example.com/job/JOB/batch",
72
+ adding_headers=mixin.bulk.headers.return_value,
73
+ body="TEST",
74
+ )
75
+ assert (
76
+ mixin._job_state_from_batches("JOB") == mixin._parse_job_state.return_value
77
+ )
78
+ mixin._parse_job_state.assert_called_once_with(b"TEST")
79
+
80
+ def test_parse_job_state(self):
81
+ mixin = BulkJobMixin()
82
+ mixin.bulk = mock.Mock()
83
+ mixin.bulk.jobNS = "http://ns"
84
+
85
+ assert mixin._parse_job_state(
86
+ BULK_BATCH_RESPONSE.format(
87
+ **{
88
+ "first_state": "Not Processed",
89
+ "first_message": "Test",
90
+ "second_state": "Completed",
91
+ "second_message": "",
92
+ }
93
+ )
94
+ ) == DataOperationJobResult(DataOperationStatus.ABORTED, [], 0, 0)
95
+
96
+ assert mixin._parse_job_state(
97
+ BULK_BATCH_RESPONSE.format(
98
+ **{
99
+ "first_state": "InProgress",
100
+ "first_message": "Test",
101
+ "second_state": "Completed",
102
+ "second_message": "",
103
+ }
104
+ )
105
+ ) == DataOperationJobResult(DataOperationStatus.IN_PROGRESS, [], 0, 0)
106
+
107
+ assert mixin._parse_job_state(
108
+ BULK_BATCH_RESPONSE.format(
109
+ **{
110
+ "first_state": "Failed",
111
+ "first_message": "Bad",
112
+ "second_state": "Failed",
113
+ "second_message": "Worse",
114
+ }
115
+ )
116
+ ) == DataOperationJobResult(
117
+ DataOperationStatus.JOB_FAILURE, ["Bad", "Worse"], 0, 0
118
+ )
119
+
120
+ assert mixin._parse_job_state(
121
+ BULK_BATCH_RESPONSE.format(
122
+ **{
123
+ "first_state": "Completed",
124
+ "first_message": "Test",
125
+ "second_state": "Completed",
126
+ "second_message": "",
127
+ }
128
+ )
129
+ ) == DataOperationJobResult(DataOperationStatus.SUCCESS, [], 0, 0)
130
+
131
+ assert mixin._parse_job_state(
132
+ '<root xmlns="http://ns">'
133
+ " <batch>"
134
+ " <state>Completed</state>"
135
+ " <numberRecordsFailed>200</numberRecordsFailed>"
136
+ " </batch>"
137
+ " <batch>"
138
+ " <state>Completed</state>"
139
+ " <numberRecordsFailed>200</numberRecordsFailed>"
140
+ " </batch>"
141
+ "</root>"
142
+ ) == DataOperationJobResult(
143
+ DataOperationStatus.ROW_FAILURE, [], 0, 400
144
+ ), "Multiple batches in single job"
145
+
146
+ assert mixin._parse_job_state(
147
+ '<root xmlns="http://ns">'
148
+ " <batch>"
149
+ " <state>Completed</state>"
150
+ " <numberRecordsFailed>200</numberRecordsFailed>"
151
+ " </batch>"
152
+ "</root>"
153
+ ) == DataOperationJobResult(
154
+ DataOperationStatus.ROW_FAILURE, [], 0, 200
155
+ ), "Single batch"
156
+
157
+ assert mixin._parse_job_state(
158
+ '<root xmlns="http://ns">'
159
+ " <batch>"
160
+ " <state>Completed</state>"
161
+ " <numberRecordsFailed>200</numberRecordsFailed>"
162
+ " <numberRecordsProcessed>10</numberRecordsProcessed>"
163
+ " </batch>"
164
+ " <batch>"
165
+ " <state>Completed</state>"
166
+ " <numberRecordsFailed>200</numberRecordsFailed>"
167
+ " <numberRecordsProcessed>10</numberRecordsProcessed>"
168
+ " </batch>"
169
+ "</root>"
170
+ ) == DataOperationJobResult(
171
+ DataOperationStatus.ROW_FAILURE, [], 20, 400
172
+ ), "Multiple batches in single job"
173
+
174
+ assert mixin._parse_job_state(
175
+ '<root xmlns="http://ns">'
176
+ " <batch><state>Completed</state></batch>"
177
+ " <numberRecordsFailed>200</numberRecordsFailed>"
178
+ " <numberRecordsProcessed>10</numberRecordsProcessed>"
179
+ "</root>"
180
+ ) == DataOperationJobResult(
181
+ DataOperationStatus.ROW_FAILURE, [], 10, 200
182
+ ), "Single batch"
183
+
184
+ @mock.patch("time.sleep")
185
+ def test_wait_for_job(self, sleep_patch):
186
+ mixin = BulkJobMixin()
187
+
188
+ mixin.bulk = mock.Mock()
189
+ mixin.bulk.job_status.return_value = {
190
+ "numberBatchesCompleted": 1,
191
+ "numberBatchesTotal": 1,
192
+ }
193
+ mixin._job_state_from_batches = mock.Mock(
194
+ side_effect=[
195
+ DataOperationJobResult(DataOperationStatus.IN_PROGRESS, [], 0, 0),
196
+ DataOperationJobResult(DataOperationStatus.SUCCESS, [], 0, 0),
197
+ ]
198
+ )
199
+ mixin.logger = mock.Mock()
200
+
201
+ result = mixin._wait_for_job("750000000000000")
202
+ mixin._job_state_from_batches.assert_has_calls(
203
+ [mock.call("750000000000000"), mock.call("750000000000000")]
204
+ )
205
+ assert result.status is DataOperationStatus.SUCCESS
206
+
207
+ def test_wait_for_job__failed(self):
208
+ mixin = BulkJobMixin()
209
+
210
+ mixin.bulk = mock.Mock()
211
+ mixin.bulk.job_status.return_value = {
212
+ "numberBatchesCompleted": 1,
213
+ "numberBatchesTotal": 1,
214
+ }
215
+ mixin._job_state_from_batches = mock.Mock(
216
+ return_value=DataOperationJobResult(
217
+ DataOperationStatus.JOB_FAILURE, ["Test1", "Test2"], 0, 0
218
+ )
219
+ )
220
+ mixin.logger = mock.Mock()
221
+
222
+ result = mixin._wait_for_job("750000000000000")
223
+ mixin._job_state_from_batches.assert_called_once_with("750000000000000")
224
+ assert result.status is DataOperationStatus.JOB_FAILURE
225
+
226
+ def test_wait_for_job__logs_state_messages(self):
227
+ mixin = BulkJobMixin()
228
+
229
+ mixin.bulk = mock.Mock()
230
+ mixin.bulk.job_status.return_value = {
231
+ "numberBatchesCompleted": 1,
232
+ "numberBatchesTotal": 1,
233
+ }
234
+ mixin._job_state_from_batches = mock.Mock(
235
+ return_value=DataOperationJobResult(
236
+ DataOperationStatus.JOB_FAILURE, ["Test1", "Test2"], 0, 0
237
+ )
238
+ )
239
+ mixin.logger = mock.Mock()
240
+
241
+ mixin._wait_for_job("750000000000000")
242
+ mixin.logger.error.assert_any_call("Batch failure message: Test1")
243
+ mixin.logger.error.assert_any_call("Batch failure message: Test2")
244
+
245
+
246
+ class TestBulkApiQueryOperation:
247
+ def test_query(self):
248
+ context = mock.Mock()
249
+ query = BulkApiQueryOperation(
250
+ sobject="Contact",
251
+ api_options={},
252
+ context=context,
253
+ query="SELECT Id FROM Contact",
254
+ )
255
+ query._wait_for_job = mock.Mock()
256
+ query._wait_for_job.return_value = DataOperationJobResult(
257
+ DataOperationStatus.SUCCESS, [], 0, 0
258
+ )
259
+
260
+ query.query()
261
+
262
+ assert query.job_result.status is DataOperationStatus.SUCCESS
263
+
264
+ context.bulk.create_query_job.assert_called_once_with(
265
+ "Contact", contentType="CSV"
266
+ )
267
+ context.bulk.query.assert_called_once_with(
268
+ context.bulk.create_query_job.return_value, "SELECT Id FROM Contact"
269
+ )
270
+ query._wait_for_job.assert_called_once_with(
271
+ context.bulk.create_query_job.return_value
272
+ )
273
+ context.bulk.close_job.assert_called_once_with(
274
+ context.bulk.create_query_job.return_value
275
+ )
276
+
277
+ def test_query__contextmanager(self):
278
+ context = mock.Mock()
279
+ query = BulkApiQueryOperation(
280
+ sobject="Contact",
281
+ api_options={},
282
+ context=context,
283
+ query="SELECT Id FROM Contact",
284
+ )
285
+ query._wait_for_job = mock.Mock()
286
+ query._wait_for_job.return_value = DataOperationJobResult(
287
+ DataOperationStatus.SUCCESS, [], 0, 0
288
+ )
289
+
290
+ with query:
291
+ assert query.job_result.status is DataOperationStatus.SUCCESS
292
+
293
+ def test_query__failure(self):
294
+ context = mock.Mock()
295
+ query = BulkApiQueryOperation(
296
+ sobject="Contact",
297
+ api_options={},
298
+ context=context,
299
+ query="SELECT Id FROM Contact",
300
+ )
301
+ query._wait_for_job = mock.Mock()
302
+ query._wait_for_job.return_value = DataOperationJobResult(
303
+ DataOperationStatus.JOB_FAILURE, [], 0, 0
304
+ )
305
+
306
+ query.query()
307
+
308
+ assert query.job_result.status is DataOperationStatus.JOB_FAILURE
309
+
310
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
311
+ def test_get_results(self, download_mock):
312
+ context = mock.Mock()
313
+ context.bulk.endpoint = "https://test"
314
+ context.bulk.create_query_job.return_value = "JOB"
315
+ context.bulk.query.return_value = "BATCH"
316
+ context.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
317
+
318
+ download_mock.return_value = io.StringIO(
319
+ """Id
320
+ 003000000000001
321
+ 003000000000002
322
+ 003000000000003"""
323
+ )
324
+ query = BulkApiQueryOperation(
325
+ sobject="Contact",
326
+ api_options={},
327
+ context=context,
328
+ query="SELECT Id FROM Contact",
329
+ )
330
+ query._wait_for_job = mock.Mock()
331
+ query._wait_for_job.return_value = DataOperationJobResult(
332
+ DataOperationStatus.SUCCESS, [], 0, 0
333
+ )
334
+ query.query()
335
+
336
+ results = list(query.get_results())
337
+
338
+ context.bulk.get_query_batch_result_ids.assert_called_once_with(
339
+ "BATCH", job_id="JOB"
340
+ )
341
+ download_mock.assert_called_once_with(
342
+ "https://test/job/JOB/batch/BATCH/result/RESULT", context.bulk
343
+ )
344
+
345
+ assert list(results) == [
346
+ ["003000000000001"],
347
+ ["003000000000002"],
348
+ ["003000000000003"],
349
+ ]
350
+
351
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
352
+ def test_get_results__no_results(self, download_mock):
353
+ context = mock.Mock()
354
+ context.bulk.endpoint = "https://test"
355
+ context.bulk.create_query_job.return_value = "JOB"
356
+ context.bulk.query.return_value = "BATCH"
357
+ context.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
358
+
359
+ download_mock.return_value = io.StringIO("Records not found for this query")
360
+ query = BulkApiQueryOperation(
361
+ sobject="Contact",
362
+ api_options={},
363
+ context=context,
364
+ query="SELECT Id FROM Contact",
365
+ )
366
+ query._wait_for_job = mock.Mock()
367
+ query._wait_for_job.return_value = DataOperationJobResult(
368
+ DataOperationStatus.SUCCESS, [], 0, 0
369
+ )
370
+ query.query()
371
+
372
+ results = list(query.get_results())
373
+
374
+ context.bulk.get_query_batch_result_ids.assert_called_once_with(
375
+ "BATCH", job_id="JOB"
376
+ )
377
+ download_mock.assert_called_once_with(
378
+ "https://test/job/JOB/batch/BATCH/result/RESULT", context.bulk
379
+ )
380
+
381
+ assert list(results) == []
382
+
383
+
384
+ class TestBulkApiDmlOperation:
385
+ def test_start(self):
386
+ context = mock.Mock()
387
+ context.bulk.create_job.return_value = "JOB"
388
+
389
+ step = BulkApiDmlOperation(
390
+ sobject="Contact",
391
+ operation=DataOperationType.INSERT,
392
+ api_options={},
393
+ context=context,
394
+ fields=["LastName"],
395
+ )
396
+
397
+ step.start()
398
+
399
+ context.bulk.create_job.assert_called_once_with(
400
+ "Contact",
401
+ "insert",
402
+ contentType="CSV",
403
+ concurrency="Parallel",
404
+ external_id_name=None,
405
+ )
406
+ assert step.job_id == "JOB"
407
+
408
+ def test_end(self):
409
+ context = mock.Mock()
410
+ context.bulk.create_job.return_value = "JOB"
411
+
412
+ step = BulkApiDmlOperation(
413
+ sobject="Contact",
414
+ operation=DataOperationType.INSERT,
415
+ api_options={},
416
+ context=context,
417
+ fields=["LastName"],
418
+ )
419
+ step._wait_for_job = mock.Mock()
420
+ step._wait_for_job.return_value = DataOperationJobResult(
421
+ DataOperationStatus.SUCCESS, [], 0, 0
422
+ )
423
+ step.job_id = "JOB"
424
+
425
+ step.end()
426
+
427
+ context.bulk.close_job.assert_called_once_with("JOB")
428
+ step._wait_for_job.assert_called_once_with("JOB")
429
+ assert step.job_result.status is DataOperationStatus.SUCCESS
430
+
431
+ def test_end__failed(self):
432
+ context = mock.Mock()
433
+ context.bulk.create_job.return_value = "JOB"
434
+
435
+ step = BulkApiDmlOperation(
436
+ sobject="Contact",
437
+ operation=DataOperationType.INSERT,
438
+ api_options={},
439
+ context=context,
440
+ fields=["LastName"],
441
+ )
442
+ step._wait_for_job = mock.Mock()
443
+ step._wait_for_job.return_value = DataOperationJobResult(
444
+ DataOperationStatus.JOB_FAILURE, [], 0, 0
445
+ )
446
+ step.job_id = "JOB"
447
+
448
+ step.end()
449
+
450
+ context.bulk.close_job.assert_called_once_with("JOB")
451
+ step._wait_for_job.assert_called_once_with("JOB")
452
+ assert step.job_result.status is DataOperationStatus.JOB_FAILURE
453
+
454
+ def test_contextmanager(self):
455
+ context = mock.Mock()
456
+ context.bulk.create_job.return_value = "JOB"
457
+
458
+ step = BulkApiDmlOperation(
459
+ sobject="Contact",
460
+ operation=DataOperationType.INSERT,
461
+ api_options={},
462
+ context=context,
463
+ fields=["LastName"],
464
+ )
465
+ step._wait_for_job = mock.Mock()
466
+ step._wait_for_job.return_value = DataOperationJobResult(
467
+ DataOperationStatus.SUCCESS, [], 0, 0
468
+ )
469
+ step.job_id = "JOB"
470
+
471
+ with step:
472
+ pass
473
+
474
+ context.bulk.create_job.assert_called_once_with(
475
+ "Contact",
476
+ "insert",
477
+ contentType="CSV",
478
+ concurrency="Parallel",
479
+ external_id_name=None,
480
+ )
481
+ assert step.job_id == "JOB"
482
+
483
+ context.bulk.close_job.assert_called_once_with("JOB")
484
+ step._wait_for_job.assert_called_once_with("JOB")
485
+ assert step.job_result.status is DataOperationStatus.SUCCESS
486
+
487
+ def test_serialize_csv_record(self):
488
+ context = mock.Mock()
489
+ step = BulkApiDmlOperation(
490
+ sobject="Contact",
491
+ operation=DataOperationType.INSERT,
492
+ api_options={"batch_size": 2},
493
+ context=context,
494
+ fields=["Id", "FirstName", "LastName"],
495
+ )
496
+
497
+ serialized = step._serialize_csv_record(step.fields)
498
+ assert serialized == b'"Id","FirstName","LastName"\r\n'
499
+
500
+ record = ["1", "Bob", "Ross"]
501
+ serialized = step._serialize_csv_record(record)
502
+ assert serialized == b'"1","Bob","Ross"\r\n'
503
+
504
+ record = ["col1", "multiline\ncol2"]
505
+ serialized = step._serialize_csv_record(record)
506
+ assert serialized == b'"col1","multiline\ncol2"\r\n'
507
+
508
+ def test_get_prev_record_values(self):
509
+ context = mock.Mock()
510
+ step = BulkApiDmlOperation(
511
+ sobject="Contact",
512
+ operation=DataOperationType.UPSERT,
513
+ api_options={"batch_size": 10, "update_key": "LastName"},
514
+ context=context,
515
+ fields=["LastName"],
516
+ )
517
+ results = [
518
+ [{"LastName": "Test1", "Id": "Id1"}, {"LastName": "Test2", "Id": "Id2"}]
519
+ ]
520
+ expected_record_values = [["Test1", "Id1"], ["Test2", "Id2"]]
521
+ expected_relevant_fields = ("Id", "LastName")
522
+ step.bulk.create_query_job = mock.Mock()
523
+ step.bulk.create_query_job.return_value = "JOB_ID"
524
+ step.bulk.query = mock.Mock()
525
+ step.bulk.query.return_value = "BATCH_ID"
526
+ step.bulk.get_all_results_for_query_batch = mock.Mock()
527
+ step.bulk.get_all_results_for_query_batch.return_value = results
528
+
529
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
530
+ with mock.patch("json.load", side_effect=lambda result: result), mock.patch(
531
+ "salesforce_bulk.util.IteratorBytesIO", side_effect=lambda result: result
532
+ ):
533
+ prev_record_values, relevant_fields = step.get_prev_record_values(records)
534
+
535
+ assert sorted(map(sorted, prev_record_values)) == sorted(
536
+ map(sorted, expected_record_values)
537
+ )
538
+ assert set(relevant_fields) == set(expected_relevant_fields)
539
+ step.bulk.create_query_job.assert_called_once_with(
540
+ "Contact", contentType="JSON"
541
+ )
542
+ step.bulk.get_all_results_for_query_batch.assert_called_once_with("BATCH_ID")
543
+
544
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
545
+ def test_select_records_standard_strategy_success(self, download_mock):
546
+ # Set up mock context and BulkApiDmlOperation
547
+ context = mock.Mock()
548
+ step = BulkApiDmlOperation(
549
+ sobject="Contact",
550
+ operation=DataOperationType.QUERY,
551
+ api_options={"batch_size": 10, "update_key": "LastName"},
552
+ context=context,
553
+ fields=["LastName"],
554
+ selection_strategy=SelectStrategy.STANDARD,
555
+ content_type="JSON",
556
+ )
557
+
558
+ # Mock Bulk API responses
559
+ step.bulk.endpoint = "https://test"
560
+ step.bulk.create_query_job.return_value = "JOB"
561
+ step.bulk.query.return_value = "BATCH"
562
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
563
+
564
+ # Mock the downloaded CSV content with a single record
565
+ download_mock.return_value = io.StringIO('[{"Id":"003000000000001"}]')
566
+
567
+ # Mock the _wait_for_job method to simulate a successful job
568
+ step._wait_for_job = mock.Mock()
569
+ step._wait_for_job.return_value = DataOperationJobResult(
570
+ DataOperationStatus.SUCCESS, [], 0, 0
571
+ )
572
+
573
+ # Prepare input records
574
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
575
+
576
+ # Execute the select_records operation
577
+ step.start()
578
+ step.select_records(records)
579
+ step.end()
580
+
581
+ # Get the results and assert their properties
582
+ results = list(step.get_results())
583
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
584
+ # Assert that all results have the expected ID, success, and created values
585
+ assert (
586
+ results.count(
587
+ DataOperationResult(
588
+ id="003000000000001", success=True, error="", created=False
589
+ )
590
+ )
591
+ == 3
592
+ )
593
+
594
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
595
+ def test_select_records_zero_load_records(self, download_mock):
596
+ # Set up mock context and BulkApiDmlOperation
597
+ context = mock.Mock()
598
+ step = BulkApiDmlOperation(
599
+ sobject="Contact",
600
+ operation=DataOperationType.QUERY,
601
+ api_options={"batch_size": 10, "update_key": "LastName"},
602
+ context=context,
603
+ fields=["LastName"],
604
+ selection_strategy=SelectStrategy.STANDARD,
605
+ content_type="JSON",
606
+ )
607
+
608
+ # Mock Bulk API responses
609
+ step.bulk.endpoint = "https://test"
610
+ step.bulk.create_query_job.return_value = "JOB"
611
+ step.bulk.query.return_value = "BATCH"
612
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
613
+
614
+ # Mock the downloaded CSV content with a single record
615
+ download_mock.return_value = io.StringIO('[{"Id":"003000000000001"}]')
616
+
617
+ # Mock the _wait_for_job method to simulate a successful job
618
+ step._wait_for_job = mock.Mock()
619
+ step._wait_for_job.return_value = DataOperationJobResult(
620
+ DataOperationStatus.SUCCESS, [], 0, 0
621
+ )
622
+
623
+ # Prepare input records
624
+ records = iter([])
625
+
626
+ # Execute the select_records operation
627
+ step.start()
628
+ step.select_records(records)
629
+ step.end()
630
+
631
+ # Get the results and assert their properties
632
+ results = list(step.get_results())
633
+ assert len(results) == 0 # Expect 0 results (no records to process)
634
+
635
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
636
+ def test_select_records_standard_strategy_failure__no_records(self, download_mock):
637
+ # Set up mock context and BulkApiDmlOperation
638
+ context = mock.Mock()
639
+ step = BulkApiDmlOperation(
640
+ sobject="Contact",
641
+ operation=DataOperationType.QUERY,
642
+ api_options={"batch_size": 10, "update_key": "LastName"},
643
+ context=context,
644
+ fields=["LastName"],
645
+ selection_strategy=SelectStrategy.STANDARD,
646
+ )
647
+
648
+ # Mock Bulk API responses
649
+ step.bulk.endpoint = "https://test"
650
+ step.bulk.create_query_job.return_value = "JOB"
651
+ step.bulk.query.return_value = "BATCH"
652
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
653
+
654
+ # Mock the downloaded CSV content indicating no records found
655
+ download_mock.return_value = io.StringIO("[]")
656
+
657
+ # Mock the _wait_for_job method to simulate a successful job
658
+ step._wait_for_job = mock.Mock()
659
+ step._wait_for_job.return_value = DataOperationJobResult(
660
+ DataOperationStatus.SUCCESS, [], 0, 0
661
+ )
662
+
663
+ # Prepare input records
664
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
665
+
666
+ # Execute the select_records operation
667
+ step.start()
668
+ step.select_records(records)
669
+ step.end()
670
+
671
+ # Get the job result and assert its properties for failure scenario
672
+ job_result = step.job_result
673
+ assert job_result.status == DataOperationStatus.JOB_FAILURE
674
+ assert (
675
+ job_result.job_errors[0]
676
+ == "No records found for Contact in the target org."
677
+ )
678
+ assert job_result.records_processed == 0
679
+ assert job_result.total_row_errors == 0
680
+
681
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
682
+ def test_select_records_user_selection_filter_success(self, download_mock):
683
+ # Set up mock context and BulkApiDmlOperation
684
+ context = mock.Mock()
685
+ step = BulkApiDmlOperation(
686
+ sobject="Contact",
687
+ operation=DataOperationType.QUERY,
688
+ api_options={"batch_size": 10, "update_key": "LastName"},
689
+ context=context,
690
+ fields=["LastName"],
691
+ selection_strategy=SelectStrategy.STANDARD,
692
+ selection_filter='WHERE LastName in ("Sample Name")',
693
+ )
694
+
695
+ # Mock Bulk API responses
696
+ step.bulk.endpoint = "https://test"
697
+ step.bulk.create_query_job.return_value = "JOB"
698
+ step.bulk.query.return_value = "BATCH"
699
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
700
+
701
+ # Mock the downloaded CSV content with a single record
702
+ download_mock.return_value = io.StringIO('[{"Id":"003000000000001"}]')
703
+
704
+ # Mock the _wait_for_job method to simulate a successful job
705
+ step._wait_for_job = mock.Mock()
706
+ step._wait_for_job.return_value = DataOperationJobResult(
707
+ DataOperationStatus.SUCCESS, [], 0, 0
708
+ )
709
+
710
+ # Prepare input records
711
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
712
+
713
+ # Execute the select_records operation
714
+ step.start()
715
+ step.select_records(records)
716
+ step.end()
717
+
718
+ # Get the results and assert their properties
719
+ results = list(step.get_results())
720
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
721
+ # Assert that all results have the expected ID, success, and created values
722
+ assert (
723
+ results.count(
724
+ DataOperationResult(
725
+ id="003000000000001", success=True, error="", created=False
726
+ )
727
+ )
728
+ == 3
729
+ )
730
+
731
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
732
+ def test_select_records_user_selection_filter_order_success(self, download_mock):
733
+ # Set up mock context and BulkApiDmlOperation
734
+ context = mock.Mock()
735
+ step = BulkApiDmlOperation(
736
+ sobject="Contact",
737
+ operation=DataOperationType.QUERY,
738
+ api_options={"batch_size": 10, "update_key": "LastName"},
739
+ context=context,
740
+ fields=["LastName"],
741
+ selection_strategy=SelectStrategy.STANDARD,
742
+ selection_filter="ORDER BY CreatedDate",
743
+ )
744
+
745
+ # Mock Bulk API responses
746
+ step.bulk.endpoint = "https://test"
747
+ step.bulk.create_query_job.return_value = "JOB"
748
+ step.bulk.query.return_value = "BATCH"
749
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
750
+
751
+ # Mock the downloaded CSV content with a single record
752
+ download_mock.return_value = io.StringIO(
753
+ '[{"Id":"003000000000003"}, {"Id":"003000000000001"}, {"Id":"003000000000002"}]'
754
+ )
755
+ # Mock the _wait_for_job method to simulate a successful job
756
+ step._wait_for_job = mock.Mock()
757
+ step._wait_for_job.return_value = DataOperationJobResult(
758
+ DataOperationStatus.SUCCESS, [], 0, 0
759
+ )
760
+
761
+ # Prepare input records
762
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
763
+
764
+ # Execute the select_records operation
765
+ step.start()
766
+ step.select_records(records)
767
+ step.end()
768
+
769
+ # Get the results and assert their properties
770
+ results = list(step.get_results())
771
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
772
+ # Assert that all results are in the order given by user query
773
+ assert results[0].id == "003000000000003"
774
+ assert results[1].id == "003000000000001"
775
+ assert results[2].id == "003000000000002"
776
+
777
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
778
+ def test_select_records_user_selection_filter_failure(self, download_mock):
779
+ # Set up mock context and BulkApiDmlOperation
780
+ context = mock.Mock()
781
+ step = BulkApiDmlOperation(
782
+ sobject="Contact",
783
+ operation=DataOperationType.QUERY,
784
+ api_options={"batch_size": 10, "update_key": "LastName"},
785
+ context=context,
786
+ fields=["LastName"],
787
+ selection_strategy=SelectStrategy.STANDARD,
788
+ selection_filter='WHERE LastName in ("Sample Name")',
789
+ )
790
+
791
+ # Mock Bulk API responses
792
+ step.bulk.endpoint = "https://test"
793
+ step.bulk.create_query_job.return_value = "JOB"
794
+ step.bulk.query.return_value = "BATCH"
795
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
796
+
797
+ # Mock the downloaded CSV content with a single record
798
+ download_mock.side_effect = BulkDataException("MALFORMED QUERY")
799
+ # Prepare input records
800
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
801
+
802
+ # Execute the select_records operation
803
+ step.start()
804
+ with pytest.raises(BulkDataException):
805
+ step.select_records(records)
806
+
807
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
808
+ def test_select_records_similarity_strategy_success(self, download_mock):
809
+ # Set up mock context and BulkApiDmlOperation
810
+ context = mock.Mock()
811
+ step = BulkApiDmlOperation(
812
+ sobject="Contact",
813
+ operation=DataOperationType.QUERY,
814
+ api_options={"batch_size": 10, "update_key": "LastName"},
815
+ context=context,
816
+ fields=["Name", "Email"],
817
+ selection_strategy=SelectStrategy.SIMILARITY,
818
+ )
819
+
820
+ # Mock Bulk API responses
821
+ step.bulk.endpoint = "https://test"
822
+ step.bulk.create_query_job.return_value = "JOB"
823
+ step.bulk.query.return_value = "BATCH"
824
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
825
+
826
+ # Mock the downloaded CSV content with a single record
827
+ download_mock.return_value = io.StringIO(
828
+ """[{"Id":"003000000000001", "Name":"Jawad", "Email":"mjawadtp@example.com"}, {"Id":"003000000000002", "Name":"Aditya", "Email":"aditya@example.com"}, {"Id":"003000000000003", "Name":"Tom", "Email":"tom@example.com"}]"""
829
+ )
830
+
831
+ # Mock the _wait_for_job method to simulate a successful job
832
+ step._wait_for_job = mock.Mock()
833
+ step._wait_for_job.return_value = DataOperationJobResult(
834
+ DataOperationStatus.SUCCESS, [], 0, 0
835
+ )
836
+
837
+ # Prepare input records
838
+ records = iter(
839
+ [
840
+ ["Jawad", "mjawadtp@example.com"],
841
+ ["Aditya", "aditya@example.com"],
842
+ ["Tom", "cruise@example.com"],
843
+ ]
844
+ )
845
+
846
+ # Execute the select_records operation
847
+ step.start()
848
+ step.select_records(records)
849
+ step.end()
850
+
851
+ # Get the results and assert their properties
852
+ results = list(step.get_results())
853
+
854
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
855
+ # Assert that all results have the expected ID, success, and created values
856
+ assert (
857
+ results.count(
858
+ DataOperationResult(
859
+ id="003000000000001", success=True, error="", created=False
860
+ )
861
+ )
862
+ == 1
863
+ )
864
+ assert (
865
+ results.count(
866
+ DataOperationResult(
867
+ id="003000000000002", success=True, error="", created=False
868
+ )
869
+ )
870
+ == 1
871
+ )
872
+ assert (
873
+ results.count(
874
+ DataOperationResult(
875
+ id="003000000000003", success=True, error="", created=False
876
+ )
877
+ )
878
+ == 1
879
+ )
880
+
881
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
882
+ def test_select_records_similarity_strategy_failure__no_records(
883
+ self, download_mock
884
+ ):
885
+ # Set up mock context and BulkApiDmlOperation
886
+ context = mock.Mock()
887
+ step = BulkApiDmlOperation(
888
+ sobject="Contact",
889
+ operation=DataOperationType.QUERY,
890
+ api_options={"batch_size": 10, "update_key": "LastName"},
891
+ context=context,
892
+ fields=["Id", "Name", "Email"],
893
+ selection_strategy=SelectStrategy.SIMILARITY,
894
+ )
895
+
896
+ # Mock Bulk API responses
897
+ step.bulk.endpoint = "https://test"
898
+ step.bulk.create_query_job.return_value = "JOB"
899
+ step.bulk.query.return_value = "BATCH"
900
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
901
+
902
+ # Mock the downloaded CSV content indicating no records found
903
+ download_mock.return_value = io.StringIO("[]")
904
+
905
+ # Mock the _wait_for_job method to simulate a successful job
906
+ step._wait_for_job = mock.Mock()
907
+ step._wait_for_job.return_value = DataOperationJobResult(
908
+ DataOperationStatus.SUCCESS, [], 0, 0
909
+ )
910
+
911
+ # Prepare input records
912
+ records = iter(
913
+ [
914
+ ["Jawad", "mjawadtp@example.com"],
915
+ ["Aditya", "aditya@example.com"],
916
+ ["Tom", "cruise@example.com"],
917
+ ]
918
+ )
919
+
920
+ # Execute the select_records operation
921
+ step.start()
922
+ step.select_records(records)
923
+ step.end()
924
+
925
+ # Get the job result and assert its properties for failure scenario
926
+ job_result = step.job_result
927
+ assert job_result.status == DataOperationStatus.JOB_FAILURE
928
+ assert (
929
+ job_result.job_errors[0]
930
+ == "No records found for Contact in the target org."
931
+ )
932
+ assert job_result.records_processed == 0
933
+ assert job_result.total_row_errors == 0
934
+
935
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
936
+ def test_select_records_similarity_strategy_parent_level_records__polymorphic(
937
+ self, download_mock
938
+ ):
939
+ mock_describe_calls()
940
+ # Set up mock context and BulkApiDmlOperation
941
+ context = mock.Mock()
942
+ step = BulkApiDmlOperation(
943
+ sobject="Event",
944
+ operation=DataOperationType.QUERY,
945
+ api_options={"batch_size": 10},
946
+ context=context,
947
+ fields=[
948
+ "Subject",
949
+ "Who.Contact.Name",
950
+ "Who.Contact.Email",
951
+ "Who.Lead.Name",
952
+ "Who.Lead.Company",
953
+ "WhoId",
954
+ ],
955
+ selection_strategy=SelectStrategy.SIMILARITY,
956
+ )
957
+
958
+ # Mock Bulk API responses
959
+ step.bulk.endpoint = "https://test"
960
+ step.bulk.create_query_job.return_value = "JOB"
961
+ step.bulk.query.return_value = "BATCH"
962
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
963
+
964
+ download_mock.return_value = io.StringIO(
965
+ """[
966
+ {"Id": "003000000000001", "Subject": "Sample Event 1", "Who":{ "attributes": {"type": "Contact"}, "Id": "abcd1234", "Name": "Sample Contact", "Email": "contact@example.com"}},
967
+ { "Id": "003000000000002", "Subject": "Sample Event 2", "Who":{ "attributes": {"type": "Lead"}, "Id": "qwer1234", "Name": "Sample Lead", "Company": "Salesforce"}}
968
+ ]"""
969
+ )
970
+
971
+ records = iter(
972
+ [
973
+ [
974
+ "Sample Event 1",
975
+ "Sample Contact",
976
+ "contact@example.com",
977
+ "",
978
+ "",
979
+ "lkjh1234",
980
+ ],
981
+ ["Sample Event 2", "", "", "Sample Lead", "Salesforce", "poiu1234"],
982
+ ]
983
+ )
984
+ step.start()
985
+ step.select_records(records)
986
+ step.end()
987
+
988
+ # Get the results and assert their properties
989
+ results = list(step.get_results())
990
+ assert len(results) == 2 # Expect 2 results (matching the input records count)
991
+
992
+ # Assert that all results have the expected ID, success, and created values
993
+ assert results[0] == DataOperationResult(
994
+ id="003000000000001", success=True, error="", created=False
995
+ )
996
+ assert results[1] == DataOperationResult(
997
+ id="003000000000002", success=True, error="", created=False
998
+ )
999
+
1000
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1001
+ def test_select_records_similarity_strategy_parent_level_records__non_polymorphic(
1002
+ self, download_mock
1003
+ ):
1004
+ mock_describe_calls()
1005
+ # Set up mock context and BulkApiDmlOperation
1006
+ context = mock.Mock()
1007
+ step = BulkApiDmlOperation(
1008
+ sobject="Contact",
1009
+ operation=DataOperationType.QUERY,
1010
+ api_options={"batch_size": 10},
1011
+ context=context,
1012
+ fields=["Name", "Account.Name", "Account.AccountNumber", "AccountId"],
1013
+ selection_strategy=SelectStrategy.SIMILARITY,
1014
+ )
1015
+
1016
+ # Mock Bulk API responses
1017
+ step.bulk.endpoint = "https://test"
1018
+ step.bulk.create_query_job.return_value = "JOB"
1019
+ step.bulk.query.return_value = "BATCH"
1020
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1021
+
1022
+ download_mock.return_value = io.StringIO(
1023
+ """[
1024
+ {"Id": "003000000000001", "Name": "Sample Contact 1", "Account":{ "attributes": {"type": "Account"}, "Id": "abcd1234", "Name": "Sample Account", "AccountNumber": 123456}},
1025
+ { "Id": "003000000000002", "Subject": "Sample Contact 2", "Account": null}
1026
+ ]"""
1027
+ )
1028
+
1029
+ records = iter(
1030
+ [
1031
+ ["Sample Contact 3", "Sample Account", "123456", "poiu1234"],
1032
+ ["Sample Contact 4", "", "", ""],
1033
+ ]
1034
+ )
1035
+ step.start()
1036
+ step.select_records(records)
1037
+ step.end()
1038
+
1039
+ # Get the results and assert their properties
1040
+ results = list(step.get_results())
1041
+ assert len(results) == 2 # Expect 2 results (matching the input records count)
1042
+
1043
+ # Assert that all results have the expected ID, success, and created values
1044
+ assert results[0] == DataOperationResult(
1045
+ id="003000000000001", success=True, error="", created=False
1046
+ )
1047
+ assert results[1] == DataOperationResult(
1048
+ id="003000000000002", success=True, error="", created=False
1049
+ )
1050
+
1051
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1052
+ def test_select_records_similarity_strategy_priority_fields(self, download_mock):
1053
+ mock_describe_calls()
1054
+ # Set up mock context and BulkApiDmlOperation
1055
+ context = mock.Mock()
1056
+ step_1 = BulkApiDmlOperation(
1057
+ sobject="Contact",
1058
+ operation=DataOperationType.QUERY,
1059
+ api_options={"batch_size": 10},
1060
+ context=context,
1061
+ fields=[
1062
+ "Name",
1063
+ "Email",
1064
+ "Account.Name",
1065
+ "Account.AccountNumber",
1066
+ "AccountId",
1067
+ ],
1068
+ selection_strategy=SelectStrategy.SIMILARITY,
1069
+ selection_priority_fields={"Name": "Name", "Email": "Email"},
1070
+ )
1071
+
1072
+ step_2 = BulkApiDmlOperation(
1073
+ sobject="Contact",
1074
+ operation=DataOperationType.QUERY,
1075
+ api_options={"batch_size": 10},
1076
+ context=context,
1077
+ fields=[
1078
+ "Name",
1079
+ "Email",
1080
+ "Account.Name",
1081
+ "Account.AccountNumber",
1082
+ "AccountId",
1083
+ ],
1084
+ selection_strategy=SelectStrategy.SIMILARITY,
1085
+ selection_priority_fields={
1086
+ "Account.Name": "Account.Name",
1087
+ "Account.AccountNumber": "Account.AccountNumber",
1088
+ },
1089
+ )
1090
+
1091
+ # Mock Bulk API responses
1092
+ step_1.bulk.endpoint = "https://test"
1093
+ step_1.bulk.create_query_job.return_value = "JOB"
1094
+ step_1.bulk.query.return_value = "BATCH"
1095
+ step_1.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1096
+ step_2.bulk.endpoint = "https://test"
1097
+ step_2.bulk.create_query_job.return_value = "JOB"
1098
+ step_2.bulk.query.return_value = "BATCH"
1099
+ step_2.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1100
+
1101
+ sample_response = [
1102
+ {
1103
+ "Id": "003000000000001",
1104
+ "Name": "Bob The Builder",
1105
+ "Email": "bob@yahoo.org",
1106
+ "Account": {
1107
+ "attributes": {"type": "Account"},
1108
+ "Id": "abcd1234",
1109
+ "Name": "Jawad TP",
1110
+ "AccountNumber": 567890,
1111
+ },
1112
+ },
1113
+ {
1114
+ "Id": "003000000000002",
1115
+ "Name": "Tom Cruise",
1116
+ "Email": "tom@exmaple.com",
1117
+ "Account": {
1118
+ "attributes": {"type": "Account"},
1119
+ "Id": "qwer1234",
1120
+ "Name": "Aditya B",
1121
+ "AccountNumber": 123456,
1122
+ },
1123
+ },
1124
+ ]
1125
+
1126
+ download_mock.side_effect = [
1127
+ io.StringIO(f"""{json.dumps(sample_response)}"""),
1128
+ io.StringIO(f"""{json.dumps(sample_response)}"""),
1129
+ ]
1130
+
1131
+ records = iter(
1132
+ [
1133
+ ["Bob The Builder", "bob@yahoo.org", "Aditya B", "123456", "poiu1234"],
1134
+ ]
1135
+ )
1136
+ records_1, records_2 = tee(records)
1137
+ step_1.start()
1138
+ step_1.select_records(records_1)
1139
+ step_1.end()
1140
+
1141
+ step_2.start()
1142
+ step_2.select_records(records_2)
1143
+ step_2.end()
1144
+
1145
+ # Get the results and assert their properties
1146
+ results_1 = list(step_1.get_results())
1147
+ results_2 = list(step_2.get_results())
1148
+ assert (
1149
+ len(results_1) == 1
1150
+ ) # Expect 1 results (matching the input records count)
1151
+ assert (
1152
+ len(results_2) == 1
1153
+ ) # Expect 1 results (matching the input records count)
1154
+
1155
+ # Assert that all results have the expected ID, success, and created values
1156
+ # Prioritizes Name and Email
1157
+ assert results_1[0] == DataOperationResult(
1158
+ id="003000000000001", success=True, error="", created=False
1159
+ )
1160
+ # Prioritizes Account.Name and Account.AccountNumber
1161
+ assert results_2[0] == DataOperationResult(
1162
+ id="003000000000002", success=True, error="", created=False
1163
+ )
1164
+
1165
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1166
+ def test_process_insert_records_success(self, download_mock):
1167
+ # Mock context and insert records
1168
+ context = mock.Mock()
1169
+ insert_records = iter([["John", "Doe"], ["Jane", "Smith"]])
1170
+ selected_records = [None, None]
1171
+
1172
+ # Mock insert fields splitting
1173
+ insert_fields = ["FirstName", "LastName"]
1174
+ with mock.patch(
1175
+ "cumulusci.tasks.bulkdata.step.split_and_filter_fields",
1176
+ return_value=(insert_fields, None),
1177
+ ) as split_mock:
1178
+ step = BulkApiDmlOperation(
1179
+ sobject="Contact",
1180
+ operation=DataOperationType.QUERY,
1181
+ api_options={"batch_size": 10},
1182
+ context=context,
1183
+ fields=["FirstName", "LastName"],
1184
+ )
1185
+
1186
+ # Mock Bulk API
1187
+ step.bulk.endpoint = "https://test"
1188
+ step.bulk.create_insert_job.return_value = "JOB"
1189
+ step.bulk.get_insert_batch_result_ids.return_value = ["RESULT"]
1190
+
1191
+ # Mock the downloaded CSV content with successful results
1192
+ download_mock.return_value = io.StringIO(
1193
+ "Id,Success,Created\n0011k00003E8xAaAAI,true,true\n0011k00003E8xAbAAJ,true,true\n"
1194
+ )
1195
+
1196
+ # Mock sub-operation for BulkApiDmlOperation
1197
+ insert_step = mock.Mock(spec=BulkApiDmlOperation)
1198
+ insert_step.start = mock.Mock()
1199
+ insert_step.load_records = mock.Mock()
1200
+ insert_step.end = mock.Mock()
1201
+ insert_step.batch_ids = ["BATCH1"]
1202
+ insert_step.bulk = mock.Mock()
1203
+ insert_step.bulk.endpoint = "https://test"
1204
+ insert_step.job_id = "JOB"
1205
+
1206
+ with mock.patch(
1207
+ "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation",
1208
+ return_value=insert_step,
1209
+ ):
1210
+ step._process_insert_records(insert_records, selected_records)
1211
+
1212
+ # Assertions for split fields and sub-operation
1213
+ split_mock.assert_called_once_with(fields=["FirstName", "LastName"])
1214
+ insert_step.start.assert_called_once()
1215
+ insert_step.load_records.assert_called_once_with(insert_records)
1216
+ insert_step.end.assert_called_once()
1217
+
1218
+ # Validate the download file interactions
1219
+ download_mock.assert_called_once_with(
1220
+ "https://test/job/JOB/batch/BATCH1/result", insert_step.bulk
1221
+ )
1222
+
1223
+ # Validate that selected_records is updated with insert results
1224
+ assert selected_records == [
1225
+ {"id": "0011k00003E8xAaAAI", "success": True, "created": True},
1226
+ {"id": "0011k00003E8xAbAAJ", "success": True, "created": True},
1227
+ ]
1228
+
1229
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1230
+ def test_process_insert_records_failure(self, download_mock):
1231
+ # Mock context and insert records
1232
+ context = mock.Mock()
1233
+ insert_records = iter([["John", "Doe"], ["Jane", "Smith"]])
1234
+ selected_records = [None, None]
1235
+
1236
+ # Mock insert fields splitting
1237
+ insert_fields = ["FirstName", "LastName"]
1238
+ with mock.patch(
1239
+ "cumulusci.tasks.bulkdata.step.split_and_filter_fields",
1240
+ return_value=(insert_fields, None),
1241
+ ):
1242
+ step = BulkApiDmlOperation(
1243
+ sobject="Contact",
1244
+ operation=DataOperationType.QUERY,
1245
+ api_options={"batch_size": 10},
1246
+ context=context,
1247
+ fields=["FirstName", "LastName"],
1248
+ )
1249
+
1250
+ # Mock failure during results download
1251
+ download_mock.side_effect = Exception("Download failed")
1252
+
1253
+ # Mock sub-operation for BulkApiDmlOperation
1254
+ insert_step = mock.Mock(spec=BulkApiDmlOperation)
1255
+ insert_step.start = mock.Mock()
1256
+ insert_step.load_records = mock.Mock()
1257
+ insert_step.end = mock.Mock()
1258
+ insert_step.batch_ids = ["BATCH1"]
1259
+ insert_step.bulk = mock.Mock()
1260
+ insert_step.bulk.endpoint = "https://test"
1261
+ insert_step.job_id = "JOB"
1262
+
1263
+ with mock.patch(
1264
+ "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation",
1265
+ return_value=insert_step,
1266
+ ):
1267
+ with pytest.raises(BulkDataException) as excinfo:
1268
+ step._process_insert_records(insert_records, selected_records)
1269
+
1270
+ # Validate that the exception is raised with the correct message
1271
+ assert "Failed to download results for batch BATCH1" in str(
1272
+ excinfo.value
1273
+ )
1274
+
1275
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1276
+ def test_select_records_similarity_strategy__insert_records__non_zero_threshold(
1277
+ self, download_mock
1278
+ ):
1279
+ # Set up mock context and BulkApiDmlOperation
1280
+ context = mock.Mock()
1281
+ # Add step with threshold
1282
+ step = BulkApiDmlOperation(
1283
+ sobject="Contact",
1284
+ operation=DataOperationType.QUERY,
1285
+ api_options={"batch_size": 10, "update_key": "LastName"},
1286
+ context=context,
1287
+ fields=["Name", "Email"],
1288
+ selection_strategy=SelectStrategy.SIMILARITY,
1289
+ threshold=0.3,
1290
+ )
1291
+
1292
+ # Mock Bulk API responses
1293
+ step.bulk.endpoint = "https://test"
1294
+ step.bulk.create_query_job.return_value = "JOB"
1295
+ step.bulk.query.return_value = "BATCH"
1296
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1297
+
1298
+ # Mock the downloaded CSV content with a single record
1299
+ select_results = io.StringIO(
1300
+ """[{"Id":"003000000000001", "Name":"Jawad", "Email":"mjawadtp@example.com"}]"""
1301
+ )
1302
+ insert_results = io.StringIO(
1303
+ "Id,Success,Created\n003000000000002,true,true\n003000000000003,true,true\n"
1304
+ )
1305
+ download_mock.side_effect = [select_results, insert_results]
1306
+
1307
+ # Mock the _wait_for_job method to simulate a successful job
1308
+ step._wait_for_job = mock.Mock()
1309
+ step._wait_for_job.return_value = DataOperationJobResult(
1310
+ DataOperationStatus.SUCCESS, [], 0, 0
1311
+ )
1312
+
1313
+ # Prepare input records
1314
+ records = iter(
1315
+ [
1316
+ ["Jawad", "mjawadtp@example.com"],
1317
+ ["Aditya", "aditya@example.com"],
1318
+ ["Tom", "cruise@example.com"],
1319
+ ]
1320
+ )
1321
+
1322
+ # Mock sub-operation for BulkApiDmlOperation
1323
+ insert_step = mock.Mock(spec=BulkApiDmlOperation)
1324
+ insert_step.start = mock.Mock()
1325
+ insert_step.load_records = mock.Mock()
1326
+ insert_step.end = mock.Mock()
1327
+ insert_step.batch_ids = ["BATCH1"]
1328
+ insert_step.bulk = mock.Mock()
1329
+ insert_step.bulk.endpoint = "https://test"
1330
+ insert_step.job_id = "JOB"
1331
+
1332
+ with mock.patch(
1333
+ "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation",
1334
+ return_value=insert_step,
1335
+ ):
1336
+ # Execute the select_records operation
1337
+ step.start()
1338
+ step.select_records(records)
1339
+ step.end()
1340
+
1341
+ # Get the results and assert their properties
1342
+ results = list(step.get_results())
1343
+
1344
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
1345
+ # Assert that all results have the expected ID, success, and created values
1346
+ assert (
1347
+ results.count(
1348
+ DataOperationResult(
1349
+ id="003000000000001", success=True, error="", created=False
1350
+ )
1351
+ )
1352
+ == 1
1353
+ )
1354
+ assert (
1355
+ results.count(
1356
+ DataOperationResult(
1357
+ id="003000000000002", success=True, error="", created=True
1358
+ )
1359
+ )
1360
+ == 1
1361
+ )
1362
+ assert (
1363
+ results.count(
1364
+ DataOperationResult(
1365
+ id="003000000000003", success=True, error="", created=True
1366
+ )
1367
+ )
1368
+ == 1
1369
+ )
1370
+
1371
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1372
+ def test_select_records_similarity_strategy__insert_records__zero_threshold(
1373
+ self, download_mock
1374
+ ):
1375
+ # Set up mock context and BulkApiDmlOperation
1376
+ context = mock.Mock()
1377
+ # Add step with threshold
1378
+ step = BulkApiDmlOperation(
1379
+ sobject="Contact",
1380
+ operation=DataOperationType.QUERY,
1381
+ api_options={"batch_size": 10, "update_key": "LastName"},
1382
+ context=context,
1383
+ fields=["Name", "Email"],
1384
+ selection_strategy=SelectStrategy.SIMILARITY,
1385
+ threshold=0,
1386
+ )
1387
+
1388
+ # Mock Bulk API responses
1389
+ step.bulk.endpoint = "https://test"
1390
+ step.bulk.create_query_job.return_value = "JOB"
1391
+ step.bulk.query.return_value = "BATCH"
1392
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1393
+
1394
+ # Mock the downloaded CSV content with a single record
1395
+ select_results = io.StringIO(
1396
+ """[{"Id":"003000000000001", "Name":"Jawad", "Email":"mjawadtp@example.com"}]"""
1397
+ )
1398
+ insert_results = io.StringIO(
1399
+ "Id,Success,Created\n003000000000002,true,true\n003000000000003,true,true\n"
1400
+ )
1401
+ download_mock.side_effect = [select_results, insert_results]
1402
+
1403
+ # Mock the _wait_for_job method to simulate a successful job
1404
+ step._wait_for_job = mock.Mock()
1405
+ step._wait_for_job.return_value = DataOperationJobResult(
1406
+ DataOperationStatus.SUCCESS, [], 0, 0
1407
+ )
1408
+
1409
+ # Prepare input records
1410
+ records = iter(
1411
+ [
1412
+ ["Jawad", "mjawadtp@example.com"],
1413
+ ["Aditya", "aditya@example.com"],
1414
+ ["Tom", "cruise@example.com"],
1415
+ ]
1416
+ )
1417
+
1418
+ # Mock sub-operation for BulkApiDmlOperation
1419
+ insert_step = mock.Mock(spec=BulkApiDmlOperation)
1420
+ insert_step.start = mock.Mock()
1421
+ insert_step.load_records = mock.Mock()
1422
+ insert_step.end = mock.Mock()
1423
+ insert_step.batch_ids = ["BATCH1"]
1424
+ insert_step.bulk = mock.Mock()
1425
+ insert_step.bulk.endpoint = "https://test"
1426
+ insert_step.job_id = "JOB"
1427
+
1428
+ with mock.patch(
1429
+ "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation",
1430
+ return_value=insert_step,
1431
+ ):
1432
+ # Execute the select_records operation
1433
+ step.start()
1434
+ step.select_records(records)
1435
+ step.end()
1436
+
1437
+ # Get the results and assert their properties
1438
+ results = list(step.get_results())
1439
+
1440
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
1441
+ # Assert that all results have the expected ID, success, and created values
1442
+ assert (
1443
+ results.count(
1444
+ DataOperationResult(
1445
+ id="003000000000001", success=True, error="", created=False
1446
+ )
1447
+ )
1448
+ == 1
1449
+ )
1450
+ assert (
1451
+ results.count(
1452
+ DataOperationResult(
1453
+ id="003000000000002", success=True, error="", created=True
1454
+ )
1455
+ )
1456
+ == 1
1457
+ )
1458
+ assert (
1459
+ results.count(
1460
+ DataOperationResult(
1461
+ id="003000000000003", success=True, error="", created=True
1462
+ )
1463
+ )
1464
+ == 1
1465
+ )
1466
+
1467
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1468
+ def test_select_records_similarity_strategy__insert_records__no_select_records(
1469
+ self, download_mock
1470
+ ):
1471
+ # Set up mock context and BulkApiDmlOperation
1472
+ context = mock.Mock()
1473
+ # Add step with threshold
1474
+ step = BulkApiDmlOperation(
1475
+ sobject="Contact",
1476
+ operation=DataOperationType.QUERY,
1477
+ api_options={"batch_size": 10, "update_key": "LastName"},
1478
+ context=context,
1479
+ fields=["Name", "Email"],
1480
+ selection_strategy=SelectStrategy.SIMILARITY,
1481
+ threshold=0.3,
1482
+ )
1483
+
1484
+ # Mock Bulk API responses
1485
+ step.bulk.endpoint = "https://test"
1486
+ step.bulk.create_query_job.return_value = "JOB"
1487
+ step.bulk.query.return_value = "BATCH"
1488
+ step.bulk.get_query_batch_result_ids.return_value = ["RESULT"]
1489
+
1490
+ # Mock the downloaded CSV content with a single record
1491
+ select_results = io.StringIO("""[]""")
1492
+ insert_results = io.StringIO(
1493
+ "Id,Success,Created\n003000000000001,true,true\n003000000000002,true,true\n003000000000003,true,true\n"
1494
+ )
1495
+ download_mock.side_effect = [select_results, insert_results]
1496
+
1497
+ # Mock the _wait_for_job method to simulate a successful job
1498
+ step._wait_for_job = mock.Mock()
1499
+ step._wait_for_job.return_value = DataOperationJobResult(
1500
+ DataOperationStatus.SUCCESS, [], 0, 0
1501
+ )
1502
+
1503
+ # Prepare input records
1504
+ records = iter(
1505
+ [
1506
+ ["Jawad", "mjawadtp@example.com"],
1507
+ ["Aditya", "aditya@example.com"],
1508
+ ["Tom", "cruise@example.com"],
1509
+ ]
1510
+ )
1511
+
1512
+ # Mock sub-operation for BulkApiDmlOperation
1513
+ insert_step = mock.Mock(spec=BulkApiDmlOperation)
1514
+ insert_step.start = mock.Mock()
1515
+ insert_step.load_records = mock.Mock()
1516
+ insert_step.end = mock.Mock()
1517
+ insert_step.batch_ids = ["BATCH1"]
1518
+ insert_step.bulk = mock.Mock()
1519
+ insert_step.bulk.endpoint = "https://test"
1520
+ insert_step.job_id = "JOB"
1521
+
1522
+ with mock.patch(
1523
+ "cumulusci.tasks.bulkdata.step.BulkApiDmlOperation",
1524
+ return_value=insert_step,
1525
+ ):
1526
+ # Execute the select_records operation
1527
+ step.start()
1528
+ step.select_records(records)
1529
+ step.end()
1530
+
1531
+ # Get the results and assert their properties
1532
+ results = list(step.get_results())
1533
+
1534
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
1535
+ # Assert that all results have the expected ID, success, and created values
1536
+ assert (
1537
+ results.count(
1538
+ DataOperationResult(
1539
+ id="003000000000001", success=True, error="", created=True
1540
+ )
1541
+ )
1542
+ == 1
1543
+ )
1544
+ assert (
1545
+ results.count(
1546
+ DataOperationResult(
1547
+ id="003000000000002", success=True, error="", created=True
1548
+ )
1549
+ )
1550
+ == 1
1551
+ )
1552
+ assert (
1553
+ results.count(
1554
+ DataOperationResult(
1555
+ id="003000000000003", success=True, error="", created=True
1556
+ )
1557
+ )
1558
+ == 1
1559
+ )
1560
+
1561
+ def test_batch(self):
1562
+ context = mock.Mock()
1563
+
1564
+ step = BulkApiDmlOperation(
1565
+ sobject="Contact",
1566
+ operation=DataOperationType.INSERT,
1567
+ api_options={"batch_size": 2},
1568
+ context=context,
1569
+ fields=["LastName"],
1570
+ )
1571
+
1572
+ records = iter([["Test"], ["Test2"], ["Test3"]])
1573
+ results = list(step._batch(records, n=2))
1574
+
1575
+ assert len(results) == 2
1576
+ assert list(results[0]) == [
1577
+ '"LastName"\r\n'.encode("utf-8"),
1578
+ '"Test"\r\n'.encode("utf-8"),
1579
+ '"Test2"\r\n'.encode("utf-8"),
1580
+ ]
1581
+ assert list(results[1]) == [
1582
+ '"LastName"\r\n'.encode("utf-8"),
1583
+ '"Test3"\r\n'.encode("utf-8"),
1584
+ ]
1585
+
1586
+ def test_batch__character_limit(self):
1587
+ context = mock.Mock()
1588
+
1589
+ step = BulkApiDmlOperation(
1590
+ sobject="Contact",
1591
+ operation=DataOperationType.INSERT,
1592
+ api_options={"batch_size": 2},
1593
+ context=context,
1594
+ fields=["LastName"],
1595
+ )
1596
+
1597
+ records = [["Test"], ["Test2"], ["Test3"]]
1598
+
1599
+ csv_rows = [step._serialize_csv_record(step.fields)]
1600
+ for r in records:
1601
+ csv_rows.append(step._serialize_csv_record(r))
1602
+
1603
+ char_limit = sum([len(r) for r in csv_rows]) - 1
1604
+
1605
+ # Ask for batches of three, but we
1606
+ # should get batches of 2 back
1607
+ results = list(step._batch(iter(records), n=3, char_limit=char_limit))
1608
+
1609
+ assert len(results) == 2
1610
+ assert list(results[0]) == [
1611
+ '"LastName"\r\n'.encode("utf-8"),
1612
+ '"Test"\r\n'.encode("utf-8"),
1613
+ '"Test2"\r\n'.encode("utf-8"),
1614
+ ]
1615
+ assert list(results[1]) == [
1616
+ '"LastName"\r\n'.encode("utf-8"),
1617
+ '"Test3"\r\n'.encode("utf-8"),
1618
+ ]
1619
+
1620
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1621
+ def test_get_results(self, download_mock):
1622
+ context = mock.Mock()
1623
+ context.bulk.endpoint = "https://test"
1624
+ download_mock.side_effect = [
1625
+ io.StringIO(
1626
+ """id,success,created,error
1627
+ 003000000000001,true,true,
1628
+ 003000000000002,true,true,"""
1629
+ ),
1630
+ io.StringIO(
1631
+ """id,success,created,error
1632
+ 003000000000003,false,false,error"""
1633
+ ),
1634
+ ]
1635
+
1636
+ step = BulkApiDmlOperation(
1637
+ sobject="Contact",
1638
+ operation=DataOperationType.INSERT,
1639
+ api_options={},
1640
+ context=context,
1641
+ fields=["LastName"],
1642
+ )
1643
+ step.job_id = "JOB"
1644
+ step.batch_ids = ["BATCH1", "BATCH2"]
1645
+
1646
+ results = step.get_results()
1647
+
1648
+ assert list(results) == [
1649
+ DataOperationResult("003000000000001", True, None, True),
1650
+ DataOperationResult("003000000000002", True, None, True),
1651
+ DataOperationResult(None, False, "error", False),
1652
+ ]
1653
+ download_mock.assert_has_calls(
1654
+ [
1655
+ mock.call("https://test/job/JOB/batch/BATCH1/result", context.bulk),
1656
+ mock.call("https://test/job/JOB/batch/BATCH2/result", context.bulk),
1657
+ ]
1658
+ )
1659
+
1660
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1661
+ def test_get_results__failure(self, download_mock):
1662
+ context = mock.Mock()
1663
+ context.bulk.endpoint = "https://test"
1664
+ download_mock.return_value.side_effect = Exception
1665
+
1666
+ step = BulkApiDmlOperation(
1667
+ sobject="Contact",
1668
+ operation=DataOperationType.INSERT,
1669
+ api_options={},
1670
+ context=context,
1671
+ fields=["LastName"],
1672
+ )
1673
+ step.job_id = "JOB"
1674
+ step.batch_ids = ["BATCH1", "BATCH2"]
1675
+
1676
+ with pytest.raises(BulkDataException):
1677
+ list(step.get_results())
1678
+
1679
+ @mock.patch("cumulusci.tasks.bulkdata.step.download_file")
1680
+ def test_end_to_end(self, download_mock):
1681
+ context = mock.Mock()
1682
+ context.bulk.endpoint = "https://test"
1683
+ context.bulk.create_job.return_value = "JOB"
1684
+ context.bulk.post_batch.side_effect = ["BATCH1", "BATCH2"]
1685
+ download_mock.return_value = io.StringIO(
1686
+ """id,success,created,error
1687
+ 003000000000001,true,true,
1688
+ 003000000000002,true,true,
1689
+ 003000000000003,false,false,error"""
1690
+ )
1691
+
1692
+ step = BulkApiDmlOperation(
1693
+ sobject="Contact",
1694
+ operation=DataOperationType.INSERT,
1695
+ api_options={},
1696
+ context=context,
1697
+ fields=["LastName"],
1698
+ )
1699
+ step._wait_for_job = mock.Mock()
1700
+ step._wait_for_job.return_value = DataOperationJobResult(
1701
+ DataOperationStatus.SUCCESS, [], 0, 0
1702
+ )
1703
+
1704
+ step.start()
1705
+ step.load_records(iter([["Test"], ["Test2"], ["Test3"]]))
1706
+ step.end()
1707
+
1708
+ assert step.job_result.status is DataOperationStatus.SUCCESS
1709
+ results = step.get_results()
1710
+
1711
+ assert list(results) == [
1712
+ DataOperationResult("003000000000001", True, None, True),
1713
+ DataOperationResult("003000000000002", True, None, True),
1714
+ DataOperationResult(None, False, "error", False),
1715
+ ]
1716
+
1717
+
1718
+ class TestRestApiQueryOperation:
1719
+ def test_query(self):
1720
+ context = mock.Mock()
1721
+ context.sf.query.return_value = {
1722
+ "totalSize": 2,
1723
+ "done": True,
1724
+ "records": [
1725
+ {
1726
+ "Id": "003000000000001",
1727
+ "LastName": "Narvaez",
1728
+ "Email": "wayne@example.com",
1729
+ },
1730
+ {"Id": "003000000000002", "LastName": "De Vries", "Email": None},
1731
+ ],
1732
+ }
1733
+
1734
+ query_op = RestApiQueryOperation(
1735
+ sobject="Contact",
1736
+ fields=["Id", "LastName", "Email"],
1737
+ api_options={},
1738
+ context=context,
1739
+ query="SELECT Id, LastName, Email FROM Contact",
1740
+ )
1741
+
1742
+ query_op.query()
1743
+
1744
+ assert query_op.job_result == DataOperationJobResult(
1745
+ DataOperationStatus.SUCCESS, [], 2, 0
1746
+ )
1747
+ assert list(query_op.get_results()) == [
1748
+ ["003000000000001", "Narvaez", "wayne@example.com"],
1749
+ ["003000000000002", "De Vries", ""],
1750
+ ]
1751
+
1752
+ def test_query_batches(self):
1753
+ context = mock.Mock()
1754
+ context.sf.query.return_value = {
1755
+ "totalSize": 2,
1756
+ "done": False,
1757
+ "records": [
1758
+ {
1759
+ "Id": "003000000000001",
1760
+ "LastName": "Narvaez",
1761
+ "Email": "wayne@example.com",
1762
+ }
1763
+ ],
1764
+ "nextRecordsUrl": "test",
1765
+ }
1766
+
1767
+ context.sf.query_more.return_value = {
1768
+ "totalSize": 2,
1769
+ "done": True,
1770
+ "records": [
1771
+ {"Id": "003000000000002", "LastName": "De Vries", "Email": None}
1772
+ ],
1773
+ }
1774
+
1775
+ query_op = RestApiQueryOperation(
1776
+ sobject="Contact",
1777
+ fields=["Id", "LastName", "Email"],
1778
+ api_options={},
1779
+ context=context,
1780
+ query="SELECT Id, LastName, Email FROM Contact",
1781
+ )
1782
+
1783
+ query_op.query()
1784
+
1785
+ assert query_op.job_result == DataOperationJobResult(
1786
+ DataOperationStatus.SUCCESS, [], 2, 0
1787
+ )
1788
+ assert list(query_op.get_results()) == [
1789
+ ["003000000000001", "Narvaez", "wayne@example.com"],
1790
+ ["003000000000002", "De Vries", ""],
1791
+ ]
1792
+
1793
+
1794
+ class TestRestApiDmlOperation:
1795
+ @responses.activate
1796
+ def test_insert_dml_operation(self):
1797
+ mock_describe_calls()
1798
+ task = _make_task(
1799
+ LoadData,
1800
+ {
1801
+ "options": {
1802
+ "database_url": "sqlite:///test.db",
1803
+ "mapping": "mapping.yml",
1804
+ }
1805
+ },
1806
+ )
1807
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
1808
+ task._init_task()
1809
+
1810
+ responses.add(
1811
+ responses.POST,
1812
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1813
+ json=[
1814
+ {"id": "003000000000001", "success": True},
1815
+ {"id": "003000000000002", "success": True},
1816
+ ],
1817
+ status=200,
1818
+ )
1819
+ responses.add(
1820
+ responses.POST,
1821
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1822
+ json=[{"id": "003000000000003", "success": True}],
1823
+ status=200,
1824
+ )
1825
+
1826
+ recs = [["Fred", "Narvaez"], [None, "De Vries"], ["Hiroko", "Aito"]]
1827
+
1828
+ dml_op = RestApiDmlOperation(
1829
+ sobject="Contact",
1830
+ operation=DataOperationType.INSERT,
1831
+ api_options={"batch_size": 2},
1832
+ context=task,
1833
+ fields=["FirstName", "LastName"],
1834
+ )
1835
+
1836
+ dml_op.start()
1837
+ dml_op.load_records(iter(recs))
1838
+ dml_op.end()
1839
+
1840
+ assert dml_op.job_result == DataOperationJobResult(
1841
+ DataOperationStatus.SUCCESS, [], 3, 0
1842
+ )
1843
+ assert list(dml_op.get_results()) == [
1844
+ DataOperationResult("003000000000001", True, "", True),
1845
+ DataOperationResult("003000000000002", True, "", True),
1846
+ DataOperationResult("003000000000003", True, "", True),
1847
+ ]
1848
+
1849
+ @responses.activate
1850
+ def test_get_prev_record_values(self):
1851
+ mock_describe_calls()
1852
+ task = _make_task(
1853
+ LoadData,
1854
+ {
1855
+ "options": {
1856
+ "database_url": "sqlite:///test.db",
1857
+ "mapping": "mapping.yml",
1858
+ }
1859
+ },
1860
+ )
1861
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
1862
+ task._init_task()
1863
+
1864
+ responses.add(
1865
+ responses.POST,
1866
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1867
+ json=[
1868
+ {"id": "003000000000001", "success": True},
1869
+ {"id": "003000000000002", "success": True},
1870
+ ],
1871
+ status=200,
1872
+ )
1873
+ responses.add(
1874
+ responses.POST,
1875
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1876
+ json=[{"id": "003000000000003", "success": True}],
1877
+ status=200,
1878
+ )
1879
+
1880
+ step = RestApiDmlOperation(
1881
+ sobject="Contact",
1882
+ operation=DataOperationType.UPSERT,
1883
+ api_options={"batch_size": 10, "update_key": "LastName"},
1884
+ context=task,
1885
+ fields=["LastName"],
1886
+ )
1887
+
1888
+ results = {
1889
+ "records": [
1890
+ {"LastName": "Test1", "Id": "Id1"},
1891
+ {"LastName": "Test2", "Id": "Id2"},
1892
+ ]
1893
+ }
1894
+ expected_record_values = [["Test1", "Id1"], ["Test2", "Id2"]]
1895
+ expected_relevant_fields = ("Id", "LastName")
1896
+ step.sf.query = mock.Mock()
1897
+ step.sf.query.return_value = results
1898
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
1899
+ prev_record_values, relevant_fields = step.get_prev_record_values(records)
1900
+
1901
+ assert sorted(map(sorted, prev_record_values)) == sorted(
1902
+ map(sorted, expected_record_values)
1903
+ )
1904
+ assert set(relevant_fields) == set(expected_relevant_fields)
1905
+
1906
+ @responses.activate
1907
+ def test_select_records_standard_strategy_success(self):
1908
+ mock_describe_calls()
1909
+ task = _make_task(
1910
+ LoadData,
1911
+ {
1912
+ "options": {
1913
+ "database_url": "sqlite:///test.db",
1914
+ "mapping": "mapping.yml",
1915
+ }
1916
+ },
1917
+ )
1918
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
1919
+ task._init_task()
1920
+
1921
+ responses.add(
1922
+ responses.POST,
1923
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1924
+ json=[
1925
+ {"id": "003000000000001", "success": True},
1926
+ {"id": "003000000000002", "success": True},
1927
+ ],
1928
+ status=200,
1929
+ )
1930
+ responses.add(
1931
+ responses.POST,
1932
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
1933
+ json=[{"id": "003000000000003", "success": True}],
1934
+ status=200,
1935
+ )
1936
+ step = RestApiDmlOperation(
1937
+ sobject="Contact",
1938
+ operation=DataOperationType.UPSERT,
1939
+ api_options={"batch_size": 10, "update_key": "LastName"},
1940
+ context=task,
1941
+ fields=["LastName"],
1942
+ selection_strategy=SelectStrategy.STANDARD,
1943
+ )
1944
+
1945
+ results = {
1946
+ "records": [
1947
+ {"Id": "003000000000001"},
1948
+ ],
1949
+ "done": True,
1950
+ }
1951
+ step.sf.restful = mock.Mock()
1952
+ step.sf.restful.return_value = results
1953
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
1954
+ step.start()
1955
+ step.select_records(records)
1956
+ step.end()
1957
+
1958
+ # Get the results and assert their properties
1959
+ results = list(step.get_results())
1960
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
1961
+ # Assert that all results have the expected ID, success, and created values
1962
+ assert (
1963
+ results.count(
1964
+ DataOperationResult(
1965
+ id="003000000000001", success=True, error="", created=False
1966
+ )
1967
+ )
1968
+ == 3
1969
+ )
1970
+
1971
+ @responses.activate
1972
+ def test_select_records_zero_load_records(self):
1973
+ mock_describe_calls()
1974
+ task = _make_task(
1975
+ LoadData,
1976
+ {
1977
+ "options": {
1978
+ "database_url": "sqlite:///test.db",
1979
+ "mapping": "mapping.yml",
1980
+ }
1981
+ },
1982
+ )
1983
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
1984
+ task._init_task()
1985
+
1986
+ step = RestApiDmlOperation(
1987
+ sobject="Contact",
1988
+ operation=DataOperationType.UPSERT,
1989
+ api_options={"batch_size": 10, "update_key": "LastName"},
1990
+ context=task,
1991
+ fields=["LastName"],
1992
+ selection_strategy=SelectStrategy.STANDARD,
1993
+ )
1994
+
1995
+ results = {
1996
+ "records": [],
1997
+ "done": True,
1998
+ }
1999
+ step.sf.restful = mock.Mock()
2000
+ step.sf.restful.return_value = results
2001
+ records = iter([])
2002
+ step.start()
2003
+ step.select_records(records)
2004
+ step.end()
2005
+
2006
+ # Get the results and assert their properties
2007
+ results = list(step.get_results())
2008
+ assert len(results) == 0 # Expect 0 results (matching the input records count)
2009
+
2010
+ @responses.activate
2011
+ def test_select_records_standard_strategy_success_pagination(self):
2012
+ mock_describe_calls()
2013
+ task = _make_task(
2014
+ LoadData,
2015
+ {
2016
+ "options": {
2017
+ "database_url": "sqlite:///test.db",
2018
+ "mapping": "mapping.yml",
2019
+ }
2020
+ },
2021
+ )
2022
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2023
+ task._init_task()
2024
+
2025
+ responses.add(
2026
+ responses.POST,
2027
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2028
+ json=[
2029
+ {"id": "003000000000001", "success": True},
2030
+ {"id": "003000000000002", "success": True},
2031
+ ],
2032
+ status=200,
2033
+ )
2034
+ responses.add(
2035
+ responses.POST,
2036
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2037
+ json=[{"id": "003000000000003", "success": True}],
2038
+ status=200,
2039
+ )
2040
+ step = RestApiDmlOperation(
2041
+ sobject="Contact",
2042
+ operation=DataOperationType.UPSERT,
2043
+ api_options={"batch_size": 10, "update_key": "LastName"},
2044
+ context=task,
2045
+ fields=["LastName"],
2046
+ selection_strategy=SelectStrategy.STANDARD,
2047
+ )
2048
+
2049
+ # Set up pagination: First call returns done=False, second call returns done=True
2050
+ step.sf.restful = mock.Mock(
2051
+ side_effect=[
2052
+ {
2053
+ "records": [{"Id": "003000000000001"}, {"Id": "003000000000002"}],
2054
+ "done": False, # Pagination in progress
2055
+ "nextRecordsUrl": "/services/data/vXX.X/query/next-records",
2056
+ },
2057
+ ]
2058
+ )
2059
+
2060
+ step.sf.query_more = mock.Mock(
2061
+ side_effect=[
2062
+ {"records": [{"Id": "003000000000003"}], "done": True} # Final page
2063
+ ]
2064
+ )
2065
+
2066
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
2067
+ step.start()
2068
+ step.select_records(records)
2069
+ step.end()
2070
+
2071
+ # Get the results and assert their properties
2072
+ results = list(step.get_results())
2073
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
2074
+
2075
+ # Assert that all results have the expected ID, success, and created values
2076
+ assert (
2077
+ results.count(
2078
+ DataOperationResult(
2079
+ id="003000000000001", success=True, error="", created=False
2080
+ )
2081
+ )
2082
+ == 1
2083
+ )
2084
+ assert (
2085
+ results.count(
2086
+ DataOperationResult(
2087
+ id="003000000000002", success=True, error="", created=False
2088
+ )
2089
+ )
2090
+ == 1
2091
+ )
2092
+ assert (
2093
+ results.count(
2094
+ DataOperationResult(
2095
+ id="003000000000003", success=True, error="", created=False
2096
+ )
2097
+ )
2098
+ == 1
2099
+ )
2100
+
2101
+ @responses.activate
2102
+ def test_select_records_standard_strategy_failure__no_records(self):
2103
+ mock_describe_calls()
2104
+ task = _make_task(
2105
+ LoadData,
2106
+ {
2107
+ "options": {
2108
+ "database_url": "sqlite:///test.db",
2109
+ "mapping": "mapping.yml",
2110
+ }
2111
+ },
2112
+ )
2113
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2114
+ task._init_task()
2115
+
2116
+ responses.add(
2117
+ responses.POST,
2118
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2119
+ json=[
2120
+ {"id": "003000000000001", "success": True},
2121
+ {"id": "003000000000002", "success": True},
2122
+ ],
2123
+ status=200,
2124
+ )
2125
+ responses.add(
2126
+ responses.POST,
2127
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2128
+ json=[{"id": "003000000000003", "success": True}],
2129
+ status=200,
2130
+ )
2131
+ step = RestApiDmlOperation(
2132
+ sobject="Contact",
2133
+ operation=DataOperationType.UPSERT,
2134
+ api_options={"batch_size": 10, "update_key": "LastName"},
2135
+ context=task,
2136
+ fields=["LastName"],
2137
+ selection_strategy=SelectStrategy.STANDARD,
2138
+ )
2139
+
2140
+ results = {"records": [], "done": True}
2141
+ step.sf.restful = mock.Mock()
2142
+ step.sf.restful.return_value = results
2143
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
2144
+ step.start()
2145
+ step.select_records(records)
2146
+ step.end()
2147
+
2148
+ # Get the job result and assert its properties for failure scenario
2149
+ job_result = step.job_result
2150
+ assert job_result.status == DataOperationStatus.JOB_FAILURE
2151
+ assert (
2152
+ job_result.job_errors[0]
2153
+ == "No records found for Contact in the target org."
2154
+ )
2155
+ assert job_result.records_processed == 0
2156
+ assert job_result.total_row_errors == 0
2157
+
2158
+ @responses.activate
2159
+ def test_select_records_user_selection_filter_success(self):
2160
+ mock_describe_calls()
2161
+ task = _make_task(
2162
+ LoadData,
2163
+ {
2164
+ "options": {
2165
+ "database_url": "sqlite:///test.db",
2166
+ "mapping": "mapping.yml",
2167
+ }
2168
+ },
2169
+ )
2170
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2171
+ task._init_task()
2172
+
2173
+ responses.add(
2174
+ responses.POST,
2175
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2176
+ json=[
2177
+ {"id": "003000000000001", "success": True},
2178
+ {"id": "003000000000002", "success": True},
2179
+ ],
2180
+ status=200,
2181
+ )
2182
+ responses.add(
2183
+ responses.POST,
2184
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2185
+ json=[{"id": "003000000000003", "success": True}],
2186
+ status=200,
2187
+ )
2188
+ step = RestApiDmlOperation(
2189
+ sobject="Contact",
2190
+ operation=DataOperationType.UPSERT,
2191
+ api_options={"batch_size": 10, "update_key": "LastName"},
2192
+ context=task,
2193
+ fields=["LastName"],
2194
+ selection_strategy=SelectStrategy.STANDARD,
2195
+ selection_filter='WHERE LastName IN ("Sample Name")',
2196
+ )
2197
+
2198
+ results = {
2199
+ "records": [
2200
+ {"Id": "003000000000001"},
2201
+ ],
2202
+ "done": True,
2203
+ }
2204
+ step.sf.restful = mock.Mock()
2205
+ step.sf.restful.return_value = results
2206
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
2207
+ step.start()
2208
+ step.select_records(records)
2209
+ step.end()
2210
+
2211
+ # Get the results and assert their properties
2212
+ results = list(step.get_results())
2213
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
2214
+ # Assert that all results have the expected ID, success, and created values
2215
+ assert (
2216
+ results.count(
2217
+ DataOperationResult(
2218
+ id="003000000000001", success=True, error="", created=False
2219
+ )
2220
+ )
2221
+ == 3
2222
+ )
2223
+
2224
+ @responses.activate
2225
+ def test_select_records_user_selection_filter_order_success(self):
2226
+ mock_describe_calls()
2227
+ task = _make_task(
2228
+ LoadData,
2229
+ {
2230
+ "options": {
2231
+ "database_url": "sqlite:///test.db",
2232
+ "mapping": "mapping.yml",
2233
+ }
2234
+ },
2235
+ )
2236
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2237
+ task._init_task()
2238
+
2239
+ responses.add(
2240
+ responses.POST,
2241
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2242
+ json=[
2243
+ {"id": "003000000000001", "success": True},
2244
+ {"id": "003000000000002", "success": True},
2245
+ ],
2246
+ status=200,
2247
+ )
2248
+ responses.add(
2249
+ responses.POST,
2250
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2251
+ json=[{"id": "003000000000003", "success": True}],
2252
+ status=200,
2253
+ )
2254
+ step = RestApiDmlOperation(
2255
+ sobject="Contact",
2256
+ operation=DataOperationType.UPSERT,
2257
+ api_options={"batch_size": 10, "update_key": "LastName"},
2258
+ context=task,
2259
+ fields=["LastName"],
2260
+ selection_strategy=SelectStrategy.STANDARD,
2261
+ selection_filter="ORDER BY CreatedDate",
2262
+ )
2263
+
2264
+ results = {
2265
+ "records": [
2266
+ {"Id": "003000000000003"},
2267
+ {"Id": "003000000000001"},
2268
+ {"Id": "003000000000002"},
2269
+ ],
2270
+ "done": True,
2271
+ }
2272
+ step.sf.restful = mock.Mock()
2273
+ step.sf.restful.return_value = results
2274
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
2275
+ step.start()
2276
+ step.select_records(records)
2277
+ step.end()
2278
+
2279
+ # Get the results and assert their properties
2280
+ results = list(step.get_results())
2281
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
2282
+ # Assert that all results are in the order of user_query
2283
+ assert results[0].id == "003000000000003"
2284
+ assert results[1].id == "003000000000001"
2285
+ assert results[2].id == "003000000000002"
2286
+
2287
+ @responses.activate
2288
+ def test_select_records_user_selection_filter_failure(self):
2289
+ mock_describe_calls()
2290
+ task = _make_task(
2291
+ LoadData,
2292
+ {
2293
+ "options": {
2294
+ "database_url": "sqlite:///test.db",
2295
+ "mapping": "mapping.yml",
2296
+ }
2297
+ },
2298
+ )
2299
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2300
+ task._init_task()
2301
+
2302
+ responses.add(
2303
+ responses.POST,
2304
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2305
+ json=[
2306
+ {"id": "003000000000001", "success": True},
2307
+ {"id": "003000000000002", "success": True},
2308
+ ],
2309
+ status=200,
2310
+ )
2311
+ responses.add(
2312
+ responses.POST,
2313
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2314
+ json=[{"id": "003000000000003", "success": True}],
2315
+ status=200,
2316
+ )
2317
+ step = RestApiDmlOperation(
2318
+ sobject="Contact",
2319
+ operation=DataOperationType.UPSERT,
2320
+ api_options={"batch_size": 10, "update_key": "LastName"},
2321
+ context=task,
2322
+ fields=["LastName"],
2323
+ selection_strategy=SelectStrategy.STANDARD,
2324
+ selection_filter="MALFORMED FILTER", # Applying malformed filter
2325
+ )
2326
+
2327
+ step.sf.restful = mock.Mock()
2328
+ step.sf.restful.side_effect = Exception("MALFORMED QUERY")
2329
+ records = iter([["Test1"], ["Test2"], ["Test3"]])
2330
+ step.start()
2331
+ with pytest.raises(Exception):
2332
+ step.select_records(records)
2333
+
2334
+ @responses.activate
2335
+ def test_select_records_similarity_strategy_success(self):
2336
+ mock_describe_calls()
2337
+ task = _make_task(
2338
+ LoadData,
2339
+ {
2340
+ "options": {
2341
+ "database_url": "sqlite:///test.db",
2342
+ "mapping": "mapping.yml",
2343
+ }
2344
+ },
2345
+ )
2346
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2347
+ task._init_task()
2348
+
2349
+ responses.add(
2350
+ responses.POST,
2351
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2352
+ json=[
2353
+ {"id": "003000000000001", "success": True},
2354
+ {"id": "003000000000002", "success": True},
2355
+ ],
2356
+ status=200,
2357
+ )
2358
+ responses.add(
2359
+ responses.POST,
2360
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2361
+ json=[{"id": "003000000000003", "success": True}],
2362
+ status=200,
2363
+ )
2364
+ step = RestApiDmlOperation(
2365
+ sobject="Contact",
2366
+ operation=DataOperationType.UPSERT,
2367
+ api_options={"batch_size": 10, "update_key": "LastName"},
2368
+ context=task,
2369
+ fields=["Name", "Email"],
2370
+ selection_strategy=SelectStrategy.SIMILARITY,
2371
+ )
2372
+
2373
+ results_first_call = {
2374
+ "records": [
2375
+ {
2376
+ "Id": "003000000000001",
2377
+ "Name": "Jawad",
2378
+ "Email": "mjawadtp@example.com",
2379
+ },
2380
+ {
2381
+ "Id": "003000000000002",
2382
+ "Name": "Aditya",
2383
+ "Email": "aditya@example.com",
2384
+ },
2385
+ {
2386
+ "Id": "003000000000003",
2387
+ "Name": "Tom Cruise",
2388
+ "Email": "tomcruise@example.com",
2389
+ },
2390
+ ],
2391
+ "done": True,
2392
+ }
2393
+
2394
+ # First call returns `results_first_call`, second call returns an empty list
2395
+ step.sf.restful = mock.Mock(
2396
+ side_effect=[results_first_call, {"records": [], "done": True}]
2397
+ )
2398
+ records = iter(
2399
+ [
2400
+ ["Jawad", "mjawadtp@example.com"],
2401
+ ["Aditya", "aditya@example.com"],
2402
+ ["Tom Cruise", "tom@example.com"],
2403
+ ]
2404
+ )
2405
+ step.start()
2406
+ step.select_records(records)
2407
+ step.end()
2408
+
2409
+ # Get the results and assert their properties
2410
+ results = list(step.get_results())
2411
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
2412
+ # Assert that all results have the expected ID, success, and created values
2413
+ assert (
2414
+ results.count(
2415
+ DataOperationResult(
2416
+ id="003000000000001", success=True, error="", created=False
2417
+ )
2418
+ )
2419
+ == 1
2420
+ )
2421
+ assert (
2422
+ results.count(
2423
+ DataOperationResult(
2424
+ id="003000000000002", success=True, error="", created=False
2425
+ )
2426
+ )
2427
+ == 1
2428
+ )
2429
+ assert (
2430
+ results.count(
2431
+ DataOperationResult(
2432
+ id="003000000000003", success=True, error="", created=False
2433
+ )
2434
+ )
2435
+ == 1
2436
+ )
2437
+
2438
+ @responses.activate
2439
+ def test_select_records_similarity_strategy_failure__no_records(self):
2440
+ mock_describe_calls()
2441
+ task = _make_task(
2442
+ LoadData,
2443
+ {
2444
+ "options": {
2445
+ "database_url": "sqlite:///test.db",
2446
+ "mapping": "mapping.yml",
2447
+ }
2448
+ },
2449
+ )
2450
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2451
+ task._init_task()
2452
+
2453
+ responses.add(
2454
+ responses.POST,
2455
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2456
+ json=[
2457
+ {"id": "003000000000001", "success": True},
2458
+ {"id": "003000000000002", "success": True},
2459
+ ],
2460
+ status=200,
2461
+ )
2462
+ responses.add(
2463
+ responses.POST,
2464
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2465
+ json=[{"id": "003000000000003", "success": True}],
2466
+ status=200,
2467
+ )
2468
+ step = RestApiDmlOperation(
2469
+ sobject="Contact",
2470
+ operation=DataOperationType.UPSERT,
2471
+ api_options={"batch_size": 10, "update_key": "LastName"},
2472
+ context=task,
2473
+ fields=["Name", "Email"],
2474
+ selection_strategy=SelectStrategy.SIMILARITY,
2475
+ )
2476
+
2477
+ results = {"records": [], "done": True}
2478
+ step.sf.restful = mock.Mock()
2479
+ step.sf.restful.return_value = results
2480
+ records = iter(
2481
+ [
2482
+ ["Id: 1", "Jawad", "mjawadtp@example.com"],
2483
+ ["Id: 2", "Aditya", "aditya@example.com"],
2484
+ ["Id: 2", "Tom", "tom@example.com"],
2485
+ ]
2486
+ )
2487
+ step.start()
2488
+ step.select_records(records)
2489
+ step.end()
2490
+
2491
+ # Get the job result and assert its properties for failure scenario
2492
+ job_result = step.job_result
2493
+ assert job_result.status == DataOperationStatus.JOB_FAILURE
2494
+ assert (
2495
+ job_result.job_errors[0]
2496
+ == "No records found for Contact in the target org."
2497
+ )
2498
+ assert job_result.records_processed == 0
2499
+ assert job_result.total_row_errors == 0
2500
+
2501
+ @responses.activate
2502
+ def test_select_records_similarity_strategy_parent_level_records__polymorphic(self):
2503
+ mock_describe_calls()
2504
+ task = _make_task(
2505
+ LoadData,
2506
+ {
2507
+ "options": {
2508
+ "database_url": "sqlite:///test.db",
2509
+ "mapping": "mapping.yml",
2510
+ }
2511
+ },
2512
+ )
2513
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2514
+ task._init_task()
2515
+
2516
+ responses.add(
2517
+ responses.POST,
2518
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2519
+ json=[
2520
+ {"id": "003000000000001", "success": True},
2521
+ {"id": "003000000000002", "success": True},
2522
+ ],
2523
+ status=200,
2524
+ )
2525
+ responses.add(
2526
+ responses.POST,
2527
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2528
+ json=[{"id": "003000000000003", "success": True}],
2529
+ status=200,
2530
+ )
2531
+ step = RestApiDmlOperation(
2532
+ sobject="Event",
2533
+ operation=DataOperationType.QUERY,
2534
+ api_options={"batch_size": 10},
2535
+ context=task,
2536
+ fields=[
2537
+ "Subject",
2538
+ "Who.Contact.Name",
2539
+ "Who.Contact.Email",
2540
+ "Who.Lead.Name",
2541
+ "Who.Lead.Company",
2542
+ "WhoId",
2543
+ ],
2544
+ selection_strategy=SelectStrategy.SIMILARITY,
2545
+ )
2546
+
2547
+ step.sf.restful = mock.Mock(
2548
+ side_effect=[
2549
+ {
2550
+ "records": [
2551
+ {
2552
+ "Id": "003000000000001",
2553
+ "Subject": "Sample Event 1",
2554
+ "Who": {
2555
+ "attributes": {"type": "Contact"},
2556
+ "Id": "abcd1234",
2557
+ "Name": "Sample Contact",
2558
+ "Email": "contact@example.com",
2559
+ },
2560
+ },
2561
+ {
2562
+ "Id": "003000000000002",
2563
+ "Subject": "Sample Event 2",
2564
+ "Who": {
2565
+ "attributes": {"type": "Lead"},
2566
+ "Id": "qwer1234",
2567
+ "Name": "Sample Lead",
2568
+ "Company": "Salesforce",
2569
+ },
2570
+ },
2571
+ ],
2572
+ "done": True,
2573
+ },
2574
+ ]
2575
+ )
2576
+
2577
+ records = iter(
2578
+ [
2579
+ [
2580
+ "Sample Event 1",
2581
+ "Sample Contact",
2582
+ "contact@example.com",
2583
+ "",
2584
+ "",
2585
+ "poiu1234",
2586
+ ],
2587
+ ["Sample Event 2", "", "", "Sample Lead", "Salesforce", "lkjh1234"],
2588
+ ]
2589
+ )
2590
+ step.start()
2591
+ step.select_records(records)
2592
+ step.end()
2593
+
2594
+ # Get the results and assert their properties
2595
+ results = list(step.get_results())
2596
+ assert len(results) == 2 # Expect 2 results (matching the input records count)
2597
+
2598
+ # Assert that all results have the expected ID, success, and created values
2599
+ assert results[0] == DataOperationResult(
2600
+ id="003000000000001", success=True, error="", created=False
2601
+ )
2602
+ assert results[1] == DataOperationResult(
2603
+ id="003000000000002", success=True, error="", created=False
2604
+ )
2605
+
2606
+ @responses.activate
2607
+ def test_select_records_similarity_strategy_parent_level_records__non_polymorphic(
2608
+ self,
2609
+ ):
2610
+ mock_describe_calls()
2611
+ task = _make_task(
2612
+ LoadData,
2613
+ {
2614
+ "options": {
2615
+ "database_url": "sqlite:///test.db",
2616
+ "mapping": "mapping.yml",
2617
+ }
2618
+ },
2619
+ )
2620
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2621
+ task._init_task()
2622
+
2623
+ responses.add(
2624
+ responses.POST,
2625
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2626
+ json=[
2627
+ {"id": "003000000000001", "success": True},
2628
+ {"id": "003000000000002", "success": True},
2629
+ ],
2630
+ status=200,
2631
+ )
2632
+ responses.add(
2633
+ responses.POST,
2634
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2635
+ json=[{"id": "003000000000003", "success": True}],
2636
+ status=200,
2637
+ )
2638
+ step = RestApiDmlOperation(
2639
+ sobject="Contact",
2640
+ operation=DataOperationType.QUERY,
2641
+ api_options={"batch_size": 10},
2642
+ context=task,
2643
+ fields=["Name", "Account.Name", "Account.AccountNumber", "AccountId"],
2644
+ selection_strategy=SelectStrategy.SIMILARITY,
2645
+ )
2646
+
2647
+ step.sf.restful = mock.Mock(
2648
+ side_effect=[
2649
+ {
2650
+ "records": [
2651
+ {
2652
+ "Id": "003000000000001",
2653
+ "Name": "Sample Contact 1",
2654
+ "Account": {
2655
+ "attributes": {"type": "Account"},
2656
+ "Id": "abcd1234",
2657
+ "Name": "Sample Account",
2658
+ "AccountNumber": 123456,
2659
+ },
2660
+ },
2661
+ {
2662
+ "Id": "003000000000002",
2663
+ "Name": "Sample Contact 2",
2664
+ "Account": None,
2665
+ },
2666
+ ],
2667
+ "done": True,
2668
+ },
2669
+ ]
2670
+ )
2671
+
2672
+ records = iter(
2673
+ [
2674
+ ["Sample Contact 3", "Sample Account", "123456", "poiu1234"],
2675
+ ["Sample Contact 4", "", "", ""],
2676
+ ]
2677
+ )
2678
+ step.start()
2679
+ step.select_records(records)
2680
+ step.end()
2681
+
2682
+ # Get the results and assert their properties
2683
+ results = list(step.get_results())
2684
+ assert len(results) == 2 # Expect 2 results (matching the input records count)
2685
+
2686
+ # Assert that all results have the expected ID, success, and created values
2687
+ assert results[0] == DataOperationResult(
2688
+ id="003000000000001", success=True, error="", created=False
2689
+ )
2690
+ assert results[1] == DataOperationResult(
2691
+ id="003000000000002", success=True, error="", created=False
2692
+ )
2693
+
2694
+ @responses.activate
2695
+ def test_select_records_similarity_strategy_priority_fields(self):
2696
+ mock_describe_calls()
2697
+ task_1 = _make_task(
2698
+ LoadData,
2699
+ {
2700
+ "options": {
2701
+ "database_url": "sqlite:///test.db",
2702
+ "mapping": "mapping.yml",
2703
+ }
2704
+ },
2705
+ )
2706
+ task_1.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2707
+ task_1._init_task()
2708
+
2709
+ task_2 = _make_task(
2710
+ LoadData,
2711
+ {
2712
+ "options": {
2713
+ "database_url": "sqlite:///test.db",
2714
+ "mapping": "mapping.yml",
2715
+ }
2716
+ },
2717
+ )
2718
+ task_2.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2719
+ task_2._init_task()
2720
+
2721
+ responses.add(
2722
+ responses.POST,
2723
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2724
+ json=[
2725
+ {"id": "003000000000001", "success": True},
2726
+ {"id": "003000000000002", "success": True},
2727
+ ],
2728
+ status=200,
2729
+ )
2730
+ responses.add(
2731
+ responses.POST,
2732
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
2733
+ json=[{"id": "003000000000003", "success": True}],
2734
+ status=200,
2735
+ )
2736
+ step_1 = RestApiDmlOperation(
2737
+ sobject="Contact",
2738
+ operation=DataOperationType.QUERY,
2739
+ api_options={"batch_size": 10},
2740
+ context=task_1,
2741
+ fields=[
2742
+ "Name",
2743
+ "Email",
2744
+ "Account.Name",
2745
+ "Account.AccountNumber",
2746
+ "AccountId",
2747
+ ],
2748
+ selection_strategy=SelectStrategy.SIMILARITY,
2749
+ selection_priority_fields={"Name": "Name", "Email": "Email"},
2750
+ )
2751
+
2752
+ step_2 = RestApiDmlOperation(
2753
+ sobject="Contact",
2754
+ operation=DataOperationType.QUERY,
2755
+ api_options={"batch_size": 10},
2756
+ context=task_2,
2757
+ fields=[
2758
+ "Name",
2759
+ "Email",
2760
+ "Account.Name",
2761
+ "Account.AccountNumber",
2762
+ "AccountId",
2763
+ ],
2764
+ selection_strategy=SelectStrategy.SIMILARITY,
2765
+ selection_priority_fields={
2766
+ "Account.Name": "Account.Name",
2767
+ "Account.AccountNumber": "Account.AccountNumber",
2768
+ },
2769
+ )
2770
+
2771
+ sample_response = [
2772
+ {
2773
+ "records": [
2774
+ {
2775
+ "Id": "003000000000001",
2776
+ "Name": "Bob The Builder",
2777
+ "Email": "bob@yahoo.org",
2778
+ "Account": {
2779
+ "attributes": {"type": "Account"},
2780
+ "Id": "abcd1234",
2781
+ "Name": "Jawad TP",
2782
+ "AccountNumber": 567890,
2783
+ },
2784
+ },
2785
+ {
2786
+ "Id": "003000000000002",
2787
+ "Name": "Tom Cruise",
2788
+ "Email": "tom@exmaple.com",
2789
+ "Account": {
2790
+ "attributes": {"type": "Account"},
2791
+ "Id": "qwer1234",
2792
+ "Name": "Aditya B",
2793
+ "AccountNumber": 123456,
2794
+ },
2795
+ },
2796
+ ],
2797
+ "done": True,
2798
+ },
2799
+ ]
2800
+
2801
+ step_1.sf.restful = mock.Mock(side_effect=sample_response)
2802
+ step_2.sf.restful = mock.Mock(side_effect=sample_response)
2803
+
2804
+ records = iter(
2805
+ [
2806
+ ["Bob The Builder", "bob@yahoo.org", "Aditya B", "123456", "poiu1234"],
2807
+ ]
2808
+ )
2809
+ records_1, records_2 = tee(records)
2810
+ step_1.start()
2811
+ step_1.select_records(records_1)
2812
+ step_1.end()
2813
+
2814
+ step_2.start()
2815
+ step_2.select_records(records_2)
2816
+ step_2.end()
2817
+
2818
+ # Get the results and assert their properties
2819
+ results_1 = list(step_1.get_results())
2820
+ results_2 = list(step_2.get_results())
2821
+ assert (
2822
+ len(results_1) == 1
2823
+ ) # Expect 1 results (matching the input records count)
2824
+ assert (
2825
+ len(results_2) == 1
2826
+ ) # Expect 1 results (matching the input records count)
2827
+
2828
+ # Assert that all results have the expected ID, success, and created values
2829
+ # Prioritizes Name and Email
2830
+ assert results_1[0] == DataOperationResult(
2831
+ id="003000000000001", success=True, error="", created=False
2832
+ )
2833
+ # Prioritizes Account.Name and Account.AccountNumber
2834
+ assert results_2[0] == DataOperationResult(
2835
+ id="003000000000002", success=True, error="", created=False
2836
+ )
2837
+
2838
+ @responses.activate
2839
+ def test_process_insert_records_success(self):
2840
+ # Mock describe calls
2841
+ mock_describe_calls()
2842
+
2843
+ # Create a task and mock project config
2844
+ task = _make_task(
2845
+ LoadData,
2846
+ {
2847
+ "options": {
2848
+ "database_url": "sqlite:///test.db",
2849
+ "mapping": "mapping.yml",
2850
+ }
2851
+ },
2852
+ )
2853
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2854
+ task._init_task()
2855
+
2856
+ # Prepare inputs
2857
+ insert_records = iter(
2858
+ [
2859
+ ["Jawad", "mjawadtp@example.com"],
2860
+ ["Aditya", "aditya@example.com"],
2861
+ ["Tom Cruise", "tomcruise@example.com"],
2862
+ ]
2863
+ )
2864
+ selected_records = [None, None, None]
2865
+
2866
+ # Mock fields splitting
2867
+ insert_fields = ["Name", "Email"]
2868
+ with mock.patch(
2869
+ "cumulusci.tasks.bulkdata.step.split_and_filter_fields",
2870
+ return_value=(insert_fields, None),
2871
+ ) as split_mock:
2872
+ # Mock the instance of RestApiDmlOperation
2873
+ mock_rest_api_dml_operation = mock.create_autospec(
2874
+ RestApiDmlOperation, instance=True
2875
+ )
2876
+ mock_rest_api_dml_operation.results = [
2877
+ {"id": "003000000000001", "success": True},
2878
+ {"id": "003000000000002", "success": True},
2879
+ {"id": "003000000000003", "success": True},
2880
+ ]
2881
+
2882
+ with mock.patch(
2883
+ "cumulusci.tasks.bulkdata.step.RestApiDmlOperation",
2884
+ return_value=mock_rest_api_dml_operation,
2885
+ ):
2886
+ # Call the function
2887
+ step = RestApiDmlOperation(
2888
+ sobject="Contact",
2889
+ operation=DataOperationType.INSERT,
2890
+ api_options={"batch_size": 10},
2891
+ context=task,
2892
+ fields=["Name", "Email"],
2893
+ )
2894
+ step._process_insert_records(insert_records, selected_records)
2895
+
2896
+ # Assert the mocked splitting is called
2897
+ split_mock.assert_called_once_with(fields=["Name", "Email"])
2898
+
2899
+ # Validate that `selected_records` is updated correctly
2900
+ assert selected_records == [
2901
+ {"id": "003000000000001", "success": True},
2902
+ {"id": "003000000000002", "success": True},
2903
+ {"id": "003000000000003", "success": True},
2904
+ ]
2905
+
2906
+ # Validate the operation sequence
2907
+ mock_rest_api_dml_operation.start.assert_called_once()
2908
+ mock_rest_api_dml_operation.load_records.assert_called_once_with(
2909
+ insert_records
2910
+ )
2911
+ mock_rest_api_dml_operation.end.assert_called_once()
2912
+
2913
+ @responses.activate
2914
+ def test_process_insert_records_failure(self):
2915
+ # Mock describe calls
2916
+ mock_describe_calls()
2917
+
2918
+ # Create a task and mock project config
2919
+ task = _make_task(
2920
+ LoadData,
2921
+ {
2922
+ "options": {
2923
+ "database_url": "sqlite:///test.db",
2924
+ "mapping": "mapping.yml",
2925
+ }
2926
+ },
2927
+ )
2928
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
2929
+ task._init_task()
2930
+
2931
+ # Prepare inputs
2932
+ insert_records = iter(
2933
+ [
2934
+ ["Jawad", "mjawadtp@example.com"],
2935
+ ["Aditya", "aditya@example.com"],
2936
+ ]
2937
+ )
2938
+ selected_records = [None, None]
2939
+
2940
+ # Mock fields splitting
2941
+ insert_fields = ["Name", "Email"]
2942
+ with mock.patch(
2943
+ "cumulusci.tasks.bulkdata.step.split_and_filter_fields",
2944
+ return_value=(insert_fields, None),
2945
+ ) as split_mock:
2946
+ # Mock the instance of RestApiDmlOperation
2947
+ mock_rest_api_dml_operation = mock.create_autospec(
2948
+ RestApiDmlOperation, instance=True
2949
+ )
2950
+ mock_rest_api_dml_operation.results = (
2951
+ None # Simulate no results due to an exception
2952
+ )
2953
+
2954
+ # Simulate an exception during processing results
2955
+ mock_rest_api_dml_operation.load_records.side_effect = BulkDataException(
2956
+ "Simulated failure"
2957
+ )
2958
+
2959
+ with mock.patch(
2960
+ "cumulusci.tasks.bulkdata.step.RestApiDmlOperation",
2961
+ return_value=mock_rest_api_dml_operation,
2962
+ ):
2963
+ # Call the function and verify that it raises the expected exception
2964
+ step = RestApiDmlOperation(
2965
+ sobject="Contact",
2966
+ operation=DataOperationType.INSERT,
2967
+ api_options={"batch_size": 10},
2968
+ context=task,
2969
+ fields=["Name", "Email"],
2970
+ )
2971
+ with pytest.raises(BulkDataException):
2972
+ step._process_insert_records(insert_records, selected_records)
2973
+
2974
+ # Assert the mocked splitting is called
2975
+ split_mock.assert_called_once_with(fields=["Name", "Email"])
2976
+
2977
+ # Validate that `selected_records` remains unchanged
2978
+ assert selected_records == [None, None]
2979
+
2980
+ # Validate the operation sequence
2981
+ mock_rest_api_dml_operation.start.assert_called_once()
2982
+ mock_rest_api_dml_operation.load_records.assert_called_once_with(
2983
+ insert_records
2984
+ )
2985
+ mock_rest_api_dml_operation.end.assert_not_called()
2986
+
2987
+ @responses.activate
2988
+ def test_select_records_similarity_strategy__insert_records__non_zero_threshold(
2989
+ self,
2990
+ ):
2991
+ mock_describe_calls()
2992
+ task = _make_task(
2993
+ LoadData,
2994
+ {
2995
+ "options": {
2996
+ "database_url": "sqlite:///test.db",
2997
+ "mapping": "mapping.yml",
2998
+ }
2999
+ },
3000
+ )
3001
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3002
+ task._init_task()
3003
+
3004
+ # Create step with threshold
3005
+ step = RestApiDmlOperation(
3006
+ sobject="Contact",
3007
+ operation=DataOperationType.UPSERT,
3008
+ api_options={"batch_size": 10},
3009
+ context=task,
3010
+ fields=["Name", "Email"],
3011
+ selection_strategy=SelectStrategy.SIMILARITY,
3012
+ threshold=0.3,
3013
+ )
3014
+
3015
+ results_select_call = {
3016
+ "records": [
3017
+ {
3018
+ "Id": "003000000000001",
3019
+ "Name": "Jawad",
3020
+ "Email": "mjawadtp@example.com",
3021
+ },
3022
+ ],
3023
+ "done": True,
3024
+ }
3025
+
3026
+ results_insert_call = [
3027
+ {"id": "003000000000002", "success": True, "created": True},
3028
+ {"id": "003000000000003", "success": True, "created": True},
3029
+ ]
3030
+
3031
+ step.sf.restful = mock.Mock(
3032
+ side_effect=[results_select_call, results_insert_call]
3033
+ )
3034
+ records = iter(
3035
+ [
3036
+ ["Jawad", "mjawadtp@example.com"],
3037
+ ["Aditya", "aditya@example.com"],
3038
+ ["Tom Cruise", "tom@example.com"],
3039
+ ]
3040
+ )
3041
+ step.start()
3042
+ step.select_records(records)
3043
+ step.end()
3044
+
3045
+ # Get the results and assert their properties
3046
+ results = list(step.get_results())
3047
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
3048
+ # Assert that all results have the expected ID, success, and created values
3049
+ assert (
3050
+ results.count(
3051
+ DataOperationResult(
3052
+ id="003000000000001", success=True, error="", created=False
3053
+ )
3054
+ )
3055
+ == 1
3056
+ )
3057
+ assert (
3058
+ results.count(
3059
+ DataOperationResult(
3060
+ id="003000000000002", success=True, error="", created=True
3061
+ )
3062
+ )
3063
+ == 1
3064
+ )
3065
+ assert (
3066
+ results.count(
3067
+ DataOperationResult(
3068
+ id="003000000000003", success=True, error="", created=True
3069
+ )
3070
+ )
3071
+ == 1
3072
+ )
3073
+
3074
+ @responses.activate
3075
+ def test_select_records_similarity_strategy__insert_records__zero_threshold(self):
3076
+ mock_describe_calls()
3077
+ task = _make_task(
3078
+ LoadData,
3079
+ {
3080
+ "options": {
3081
+ "database_url": "sqlite:///test.db",
3082
+ "mapping": "mapping.yml",
3083
+ }
3084
+ },
3085
+ )
3086
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3087
+ task._init_task()
3088
+
3089
+ # Create step with threshold
3090
+ step = RestApiDmlOperation(
3091
+ sobject="Contact",
3092
+ operation=DataOperationType.UPSERT,
3093
+ api_options={"batch_size": 10},
3094
+ context=task,
3095
+ fields=["Name", "Email"],
3096
+ selection_strategy=SelectStrategy.SIMILARITY,
3097
+ threshold=0,
3098
+ )
3099
+
3100
+ results_select_call = {
3101
+ "records": [
3102
+ {
3103
+ "Id": "003000000000001",
3104
+ "Name": "Jawad",
3105
+ "Email": "mjawadtp@example.com",
3106
+ },
3107
+ ],
3108
+ "done": True,
3109
+ }
3110
+
3111
+ results_insert_call = [
3112
+ {"id": "003000000000002", "success": True, "created": True},
3113
+ {"id": "003000000000003", "success": True, "created": True},
3114
+ ]
3115
+
3116
+ step.sf.restful = mock.Mock(
3117
+ side_effect=[results_select_call, results_insert_call]
3118
+ )
3119
+ records = iter(
3120
+ [
3121
+ ["Jawad", "mjawadtp@example.com"],
3122
+ ["Aditya", "aditya@example.com"],
3123
+ ["Tom Cruise", "tom@example.com"],
3124
+ ]
3125
+ )
3126
+ step.start()
3127
+ step.select_records(records)
3128
+ step.end()
3129
+
3130
+ # Get the results and assert their properties
3131
+ results = list(step.get_results())
3132
+ assert len(results) == 3 # Expect 3 results (matching the input records count)
3133
+ # Assert that all results have the expected ID, success, and created values
3134
+ assert (
3135
+ results.count(
3136
+ DataOperationResult(
3137
+ id="003000000000001", success=True, error="", created=False
3138
+ )
3139
+ )
3140
+ == 1
3141
+ )
3142
+ assert (
3143
+ results.count(
3144
+ DataOperationResult(
3145
+ id="003000000000002", success=True, error="", created=True
3146
+ )
3147
+ )
3148
+ == 1
3149
+ )
3150
+ assert (
3151
+ results.count(
3152
+ DataOperationResult(
3153
+ id="003000000000003", success=True, error="", created=True
3154
+ )
3155
+ )
3156
+ == 1
3157
+ )
3158
+
3159
+ @responses.activate
3160
+ def test_insert_dml_operation__boolean_conversion(self):
3161
+ mock_describe_calls()
3162
+ task = _make_task(
3163
+ LoadData,
3164
+ {
3165
+ "options": {
3166
+ "database_url": "sqlite:///test.db",
3167
+ "mapping": "mapping.yml",
3168
+ }
3169
+ },
3170
+ )
3171
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3172
+ task._init_task()
3173
+
3174
+ responses.add(
3175
+ responses.POST,
3176
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3177
+ json=[
3178
+ {"id": "003000000000001", "success": True},
3179
+ {"id": "003000000000002", "success": True},
3180
+ ],
3181
+ status=200,
3182
+ )
3183
+ responses.add(
3184
+ responses.POST,
3185
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3186
+ json=[{"id": "003000000000003", "success": True}],
3187
+ status=200,
3188
+ )
3189
+
3190
+ recs = [
3191
+ ["Narvaez", "true"],
3192
+ ["Chalmers", "True"],
3193
+ ["De Vries", "False"],
3194
+ ["Aito", "false"],
3195
+ ["Boone", None],
3196
+ ["June", False],
3197
+ ["Zoom", True],
3198
+ ["Jewel", 0],
3199
+ ["Zule", 1],
3200
+ ["Jane", "0"],
3201
+ ["Zane", "1"],
3202
+ ]
3203
+
3204
+ dml_op = RestApiDmlOperation(
3205
+ sobject="Contact",
3206
+ operation=DataOperationType.INSERT,
3207
+ context=task,
3208
+ api_options={},
3209
+ fields=["LastName", "IsEmailBounced"],
3210
+ )
3211
+
3212
+ dml_op.start()
3213
+ dml_op.load_records(iter(recs))
3214
+ dml_op.end()
3215
+
3216
+ assert json.loads(responses.calls[1].request.body) == {
3217
+ "allOrNone": False,
3218
+ "records": [
3219
+ {
3220
+ "LastName": "Narvaez",
3221
+ "IsEmailBounced": True,
3222
+ "attributes": {"type": "Contact"},
3223
+ },
3224
+ {
3225
+ "LastName": "Chalmers",
3226
+ "IsEmailBounced": True,
3227
+ "attributes": {"type": "Contact"},
3228
+ },
3229
+ {
3230
+ "LastName": "De Vries",
3231
+ "IsEmailBounced": False,
3232
+ "attributes": {"type": "Contact"},
3233
+ },
3234
+ {
3235
+ "LastName": "Aito",
3236
+ "IsEmailBounced": False,
3237
+ "attributes": {"type": "Contact"},
3238
+ },
3239
+ {
3240
+ "LastName": "Boone",
3241
+ "IsEmailBounced": False,
3242
+ "attributes": {"type": "Contact"},
3243
+ },
3244
+ {
3245
+ "LastName": "June",
3246
+ "IsEmailBounced": False,
3247
+ "attributes": {"type": "Contact"},
3248
+ },
3249
+ {
3250
+ "LastName": "Zoom",
3251
+ "IsEmailBounced": True,
3252
+ "attributes": {"type": "Contact"},
3253
+ },
3254
+ {
3255
+ "LastName": "Jewel",
3256
+ "IsEmailBounced": False,
3257
+ "attributes": {"type": "Contact"},
3258
+ },
3259
+ {
3260
+ "LastName": "Zule",
3261
+ "IsEmailBounced": True,
3262
+ "attributes": {"type": "Contact"},
3263
+ },
3264
+ {
3265
+ "LastName": "Jane",
3266
+ "IsEmailBounced": False,
3267
+ "attributes": {"type": "Contact"},
3268
+ },
3269
+ {
3270
+ "LastName": "Zane",
3271
+ "IsEmailBounced": True,
3272
+ "attributes": {"type": "Contact"},
3273
+ },
3274
+ ],
3275
+ }
3276
+
3277
+ @responses.activate
3278
+ def test_insert_dml_operation__boolean_conversion__fails(self):
3279
+ mock_describe_calls()
3280
+ task = _make_task(
3281
+ LoadData,
3282
+ {
3283
+ "options": {
3284
+ "database_url": "sqlite:///test.db",
3285
+ "mapping": "mapping.yml",
3286
+ }
3287
+ },
3288
+ )
3289
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3290
+ task._init_task()
3291
+
3292
+ responses.add(
3293
+ responses.POST,
3294
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3295
+ json=[
3296
+ {"id": "003000000000001", "success": True},
3297
+ {"id": "003000000000002", "success": True},
3298
+ ],
3299
+ status=200,
3300
+ )
3301
+ responses.add(
3302
+ responses.POST,
3303
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3304
+ json=[{"id": "003000000000003", "success": True}],
3305
+ status=200,
3306
+ )
3307
+
3308
+ recs = [
3309
+ ["Narvaez", "xyzzy"],
3310
+ ]
3311
+
3312
+ dml_op = RestApiDmlOperation(
3313
+ sobject="Contact",
3314
+ operation=DataOperationType.INSERT,
3315
+ context=task,
3316
+ api_options={},
3317
+ fields=["LastName", "IsEmailBounced"],
3318
+ )
3319
+
3320
+ dml_op.start()
3321
+ with pytest.raises(BulkDataException) as e:
3322
+ dml_op.load_records(iter(recs))
3323
+ assert "xyzzy" in str(e.value)
3324
+
3325
+ @responses.activate
3326
+ def test_insert_dml_operation__row_failure(self):
3327
+ mock_describe_calls()
3328
+ task = _make_task(
3329
+ LoadData,
3330
+ {
3331
+ "options": {
3332
+ "database_url": "sqlite:///test.db",
3333
+ "mapping": "mapping.yml",
3334
+ }
3335
+ },
3336
+ )
3337
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3338
+ task._init_task()
3339
+
3340
+ responses.add(
3341
+ responses.POST,
3342
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3343
+ json=[
3344
+ {"id": "003000000000001", "success": True},
3345
+ {"id": "003000000000002", "success": True},
3346
+ ],
3347
+ status=200,
3348
+ )
3349
+ responses.add(
3350
+ responses.POST,
3351
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3352
+ json=[
3353
+ {
3354
+ "id": "003000000000003",
3355
+ "success": False,
3356
+ "errors": [
3357
+ {
3358
+ "statusCode": "VALIDATION_ERR",
3359
+ "message": "Bad data",
3360
+ "fields": ["FirstName"],
3361
+ }
3362
+ ],
3363
+ }
3364
+ ],
3365
+ status=200,
3366
+ )
3367
+
3368
+ recs = [["Fred", "Narvaez"], [None, "De Vries"], ["Hiroko", "Aito"]]
3369
+
3370
+ dml_op = RestApiDmlOperation(
3371
+ sobject="Contact",
3372
+ operation=DataOperationType.INSERT,
3373
+ api_options={"batch_size": 2},
3374
+ context=task,
3375
+ fields=["FirstName", "LastName"],
3376
+ )
3377
+
3378
+ dml_op.start()
3379
+ dml_op.load_records(iter(recs))
3380
+ dml_op.end()
3381
+
3382
+ assert dml_op.job_result == DataOperationJobResult(
3383
+ DataOperationStatus.ROW_FAILURE, [], 3, 1
3384
+ )
3385
+ assert list(dml_op.get_results()) == [
3386
+ DataOperationResult("003000000000001", True, "", True),
3387
+ DataOperationResult("003000000000002", True, "", True),
3388
+ DataOperationResult(
3389
+ "003000000000003", False, "VALIDATION_ERR: Bad data (FirstName)", True
3390
+ ),
3391
+ ]
3392
+
3393
+ @responses.activate
3394
+ def test_insert_dml_operation__delete(self):
3395
+ mock_describe_calls()
3396
+ task = _make_task(
3397
+ LoadData,
3398
+ {
3399
+ "options": {
3400
+ "database_url": "sqlite:///test.db",
3401
+ "mapping": "mapping.yml",
3402
+ }
3403
+ },
3404
+ )
3405
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3406
+ task._init_task()
3407
+
3408
+ responses.add(
3409
+ responses.DELETE,
3410
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects?ids=003000000000001,003000000000002",
3411
+ json=[
3412
+ {"id": "003000000000001", "success": True},
3413
+ {"id": "003000000000002", "success": True},
3414
+ ],
3415
+ status=200,
3416
+ )
3417
+ responses.add(
3418
+ responses.DELETE,
3419
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects?ids=003000000000003",
3420
+ json=[{"id": "003000000000003", "success": True}],
3421
+ status=200,
3422
+ )
3423
+
3424
+ recs = [["003000000000001"], ["003000000000002"], ["003000000000003"]]
3425
+
3426
+ dml_op = RestApiDmlOperation(
3427
+ sobject="Contact",
3428
+ operation=DataOperationType.DELETE,
3429
+ api_options={"batch_size": 2},
3430
+ context=task,
3431
+ fields=["Id"],
3432
+ )
3433
+
3434
+ dml_op.start()
3435
+ dml_op.load_records(iter(recs))
3436
+ dml_op.end()
3437
+
3438
+ assert dml_op.job_result == DataOperationJobResult(
3439
+ DataOperationStatus.SUCCESS, [], 3, 0
3440
+ )
3441
+ assert list(dml_op.get_results()) == [
3442
+ DataOperationResult("003000000000001", True, ""),
3443
+ DataOperationResult("003000000000002", True, ""),
3444
+ DataOperationResult("003000000000003", True, ""),
3445
+ ]
3446
+
3447
+ @responses.activate
3448
+ def test_insert_dml_operation__booleans(self):
3449
+ mock_describe_calls()
3450
+ task = _make_task(
3451
+ LoadData,
3452
+ {
3453
+ "options": {
3454
+ "database_url": "sqlite:///test.db",
3455
+ "mapping": "mapping.yml",
3456
+ }
3457
+ },
3458
+ )
3459
+ task.project_config.project__package__api_version = CURRENT_SF_API_VERSION
3460
+ task._init_task()
3461
+
3462
+ responses.add(
3463
+ responses.POST,
3464
+ url=f"https://example.com/services/data/v{CURRENT_SF_API_VERSION}/composite/sobjects",
3465
+ json=[{"id": "003000000000001", "success": True}],
3466
+ status=200,
3467
+ )
3468
+
3469
+ recs = [["Narvaez", "True"]]
3470
+ dml_op = RestApiDmlOperation(
3471
+ sobject="Contact",
3472
+ operation=DataOperationType.INSERT,
3473
+ api_options={"batch_size": 2},
3474
+ context=task,
3475
+ fields=["LastName", "IsEmailBounced"], # IsEmailBounced is a Boolean field.
3476
+ )
3477
+
3478
+ dml_op.start()
3479
+ dml_op.load_records(iter(recs))
3480
+ dml_op.end()
3481
+
3482
+ json_body = json.loads(responses.calls[1].request.body)
3483
+ assert json_body["records"] == [
3484
+ {
3485
+ "LastName": "Narvaez",
3486
+ "IsEmailBounced": True,
3487
+ "attributes": {"type": "Contact"},
3488
+ }
3489
+ ]
3490
+
3491
+
3492
+ class TestGetOperationFunctions:
3493
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3494
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3495
+ def test_get_query_operation(self, rest_query, bulk_query):
3496
+ context = mock.Mock()
3497
+ context.sf.sf_version = "42.0"
3498
+ op = get_query_operation(
3499
+ sobject="Test",
3500
+ fields=["Id"],
3501
+ api_options={},
3502
+ context=context,
3503
+ query="SELECT Id FROM Test",
3504
+ api=DataApi.BULK,
3505
+ )
3506
+ assert op == bulk_query.return_value
3507
+ bulk_query.assert_called_once_with(
3508
+ sobject="Test",
3509
+ api_options={},
3510
+ context=context,
3511
+ query="SELECT Id FROM Test",
3512
+ )
3513
+
3514
+ op = get_query_operation(
3515
+ sobject="Test",
3516
+ fields=["Id"],
3517
+ api_options={},
3518
+ context=context,
3519
+ query="SELECT Id FROM Test",
3520
+ api=DataApi.REST,
3521
+ )
3522
+ assert op == rest_query.return_value
3523
+ rest_query.assert_called_once_with(
3524
+ sobject="Test",
3525
+ fields=["Id"],
3526
+ api_options={},
3527
+ context=context,
3528
+ query="SELECT Id FROM Test",
3529
+ )
3530
+
3531
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3532
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3533
+ def test_get_query_operation__smart_to_rest(self, rest_query, bulk_query):
3534
+ context = mock.Mock()
3535
+ context.sf.restful.return_value = {"sObjects": [{"name": "Test", "count": 1}]}
3536
+ context.sf.sf_version = "42.0"
3537
+ op = get_query_operation(
3538
+ sobject="Test",
3539
+ fields=["Id"],
3540
+ api_options={},
3541
+ context=context,
3542
+ query="SELECT Id FROM Test",
3543
+ api=DataApi.SMART,
3544
+ )
3545
+ assert op == rest_query.return_value
3546
+
3547
+ bulk_query.assert_not_called()
3548
+ context.sf.restful.assert_called_once_with("limits/recordCount?sObjects=Test")
3549
+
3550
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3551
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3552
+ def test_get_query_operation__smart_to_bulk(self, rest_query, bulk_query):
3553
+ context = mock.Mock()
3554
+ context.sf.restful.return_value = {
3555
+ "sObjects": [{"name": "Test", "count": 10000}]
3556
+ }
3557
+ context.sf.sf_version = "42.0"
3558
+ op = get_query_operation(
3559
+ sobject="Test",
3560
+ fields=["Id"],
3561
+ api_options={},
3562
+ context=context,
3563
+ query="SELECT Id FROM Test",
3564
+ api=DataApi.SMART,
3565
+ )
3566
+ assert op == bulk_query.return_value
3567
+
3568
+ rest_query.assert_not_called()
3569
+ context.sf.restful.assert_called_once_with("limits/recordCount?sObjects=Test")
3570
+
3571
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3572
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3573
+ def test_get_query_operation__old_api_version(self, rest_query, bulk_query):
3574
+ context = mock.Mock()
3575
+ context.sf.sf_version = "39.0"
3576
+ op = get_query_operation(
3577
+ sobject="Test",
3578
+ fields=["Id"],
3579
+ api_options={},
3580
+ context=context,
3581
+ query="SELECT Id FROM Test",
3582
+ api=DataApi.SMART,
3583
+ )
3584
+ assert op == bulk_query.return_value
3585
+
3586
+ context.sf.restful.assert_not_called()
3587
+
3588
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3589
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3590
+ def test_get_query_operation__bad_api(self, rest_query, bulk_query):
3591
+ context = mock.Mock()
3592
+ context.sf.sf_version = "42.0"
3593
+ with pytest.raises(AssertionError, match="Unknown API"):
3594
+ get_query_operation(
3595
+ sobject="Test",
3596
+ fields=["Id"],
3597
+ api_options={},
3598
+ context=context,
3599
+ query="SELECT Id FROM Test",
3600
+ api="foo",
3601
+ )
3602
+
3603
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiQueryOperation")
3604
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiQueryOperation")
3605
+ def test_get_query_operation__inferred_api(self, rest_query, bulk_query):
3606
+ context = mock.Mock()
3607
+ context.sf.sf_version = "42.0"
3608
+ context.sf.restful.return_value = {
3609
+ "sObjects": [{"name": "Test", "count": 10000}]
3610
+ }
3611
+ op = get_query_operation(
3612
+ sobject="Test",
3613
+ fields=["Id"],
3614
+ api_options={},
3615
+ context=context,
3616
+ query="SELECT Id FROM Test",
3617
+ )
3618
+ assert op == bulk_query.return_value
3619
+
3620
+ context.sf.restful.assert_called_once_with("limits/recordCount?sObjects=Test")
3621
+
3622
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation")
3623
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation")
3624
+ def test_get_dml_operation(self, rest_dml, bulk_dml):
3625
+ context = mock.Mock()
3626
+ context.sf.sf_version = "42.0"
3627
+ op = get_dml_operation(
3628
+ sobject="Test",
3629
+ operation=DataOperationType.INSERT,
3630
+ fields=["Name"],
3631
+ api_options={},
3632
+ context=context,
3633
+ api=DataApi.BULK,
3634
+ volume=1,
3635
+ selection_strategy=SelectStrategy.SIMILARITY,
3636
+ selection_filter=None,
3637
+ )
3638
+
3639
+ assert op == bulk_dml.return_value
3640
+ bulk_dml.assert_called_once_with(
3641
+ sobject="Test",
3642
+ operation=DataOperationType.INSERT,
3643
+ fields=["Name"],
3644
+ api_options={},
3645
+ context=context,
3646
+ selection_strategy=SelectStrategy.SIMILARITY,
3647
+ selection_filter=None,
3648
+ selection_priority_fields=None,
3649
+ content_type=None,
3650
+ threshold=None,
3651
+ )
3652
+
3653
+ op = get_dml_operation(
3654
+ sobject="Test",
3655
+ operation=DataOperationType.INSERT,
3656
+ fields=["Name"],
3657
+ api_options={},
3658
+ context=context,
3659
+ api=DataApi.REST,
3660
+ volume=1,
3661
+ selection_strategy=SelectStrategy.SIMILARITY,
3662
+ selection_filter=None,
3663
+ )
3664
+
3665
+ assert op == rest_dml.return_value
3666
+ rest_dml.assert_called_once_with(
3667
+ sobject="Test",
3668
+ operation=DataOperationType.INSERT,
3669
+ fields=["Name"],
3670
+ api_options={},
3671
+ context=context,
3672
+ selection_strategy=SelectStrategy.SIMILARITY,
3673
+ selection_filter=None,
3674
+ selection_priority_fields=None,
3675
+ content_type=None,
3676
+ threshold=None,
3677
+ )
3678
+
3679
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation")
3680
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation")
3681
+ def test_get_dml_operation__smart(self, rest_dml, bulk_dml):
3682
+ context = mock.Mock()
3683
+ context.sf.sf_version = "42.0"
3684
+ assert (
3685
+ get_dml_operation(
3686
+ sobject="Test",
3687
+ operation=DataOperationType.INSERT,
3688
+ fields=["Name"],
3689
+ api_options={},
3690
+ context=context,
3691
+ api=DataApi.SMART,
3692
+ volume=1,
3693
+ )
3694
+ == rest_dml.return_value
3695
+ )
3696
+
3697
+ assert (
3698
+ get_dml_operation(
3699
+ sobject="Test",
3700
+ operation=DataOperationType.INSERT,
3701
+ fields=["Name"],
3702
+ api_options={},
3703
+ context=context,
3704
+ api=DataApi.SMART,
3705
+ volume=10000,
3706
+ )
3707
+ == bulk_dml.return_value
3708
+ )
3709
+
3710
+ assert (
3711
+ get_dml_operation(
3712
+ sobject="Test",
3713
+ operation=DataOperationType.HARD_DELETE,
3714
+ fields=["Name"],
3715
+ api_options={},
3716
+ context=context,
3717
+ api=DataApi.SMART,
3718
+ volume=1,
3719
+ )
3720
+ == bulk_dml.return_value
3721
+ )
3722
+
3723
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation")
3724
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation")
3725
+ def test_get_dml_operation__inferred_api(self, rest_dml, bulk_dml):
3726
+ context = mock.Mock()
3727
+ context.sf.sf_version = "42.0"
3728
+ assert (
3729
+ get_dml_operation(
3730
+ sobject="Test",
3731
+ operation=DataOperationType.INSERT,
3732
+ fields=["Name"],
3733
+ api_options={},
3734
+ context=context,
3735
+ volume=1,
3736
+ )
3737
+ == rest_dml.return_value
3738
+ )
3739
+
3740
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation")
3741
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation")
3742
+ def test_get_dml_operation__old_api_version(self, rest_dml, bulk_dml):
3743
+ context = mock.Mock()
3744
+ context.sf.sf_version = "39.0"
3745
+ assert (
3746
+ get_dml_operation(
3747
+ sobject="Test",
3748
+ operation=DataOperationType.INSERT,
3749
+ fields=["Name"],
3750
+ api_options={},
3751
+ context=context,
3752
+ api=DataApi.SMART,
3753
+ volume=1,
3754
+ )
3755
+ == bulk_dml.return_value
3756
+ )
3757
+
3758
+ @mock.patch("cumulusci.tasks.bulkdata.step.BulkApiDmlOperation")
3759
+ @mock.patch("cumulusci.tasks.bulkdata.step.RestApiDmlOperation")
3760
+ def test_get_dml_operation__bad_api(self, rest_dml, bulk_dml):
3761
+ context = mock.Mock()
3762
+ context.sf.sf_version = "42.0"
3763
+ with pytest.raises(AssertionError, match="Unknown API"):
3764
+ get_dml_operation(
3765
+ sobject="Test",
3766
+ operation=DataOperationType.INSERT,
3767
+ fields=["Name"],
3768
+ api_options={},
3769
+ context=context,
3770
+ api=42,
3771
+ volume=1,
3772
+ )
3773
+
3774
+ def test_cleanup_date_strings__insert(self):
3775
+ """Empty date strings should be removed from INSERT operations"""
3776
+ context = mock.Mock()
3777
+ context.sf.sf_version = "42.0"
3778
+ context.sf.Test__c.describe = lambda: {
3779
+ "name": "Test__c",
3780
+ "fields": [
3781
+ {"name": "Birthdate", "type": "date"},
3782
+ {"name": "IsHappy", "type": "boolean"},
3783
+ {"name": "Name", "type": "string"},
3784
+ ],
3785
+ }
3786
+
3787
+ step = get_dml_operation(
3788
+ sobject="Test__c",
3789
+ operation=DataOperationType.INSERT,
3790
+ fields=["Birthdate", "IsHappy", "Name"],
3791
+ api_options={},
3792
+ context=context,
3793
+ api=DataApi.REST,
3794
+ volume=1,
3795
+ )
3796
+ json_out = step._record_to_json(["", "", "Bill"])
3797
+ assert json_out == {
3798
+ "IsHappy": False,
3799
+ "Name": "Bill",
3800
+ "attributes": {"type": "Test__c"},
3801
+ }, json_out
3802
+ # Empty dates (and other fields) should be filtered out of INSERTs
3803
+ assert "BirthDate" not in json_out # just for emphasis
3804
+
3805
+ @pytest.mark.parametrize(
3806
+ "operation", ((DataOperationType.UPSERT, DataOperationType.UPDATE))
3807
+ )
3808
+ def test_cleanup_date_strings__upsert_update(self, operation):
3809
+ """Empty date strings should be NULLED for UPSERT and UPDATE operations"""
3810
+ context = mock.Mock()
3811
+ context.sf.sf_version = "42.0"
3812
+ context.sf.Test__c.describe = lambda: {
3813
+ "name": "Test__c",
3814
+ "fields": [
3815
+ {"name": "Birthdate", "type": "date"},
3816
+ {"name": "IsHappy", "type": "boolean"},
3817
+ {"name": "Name", "type": "string"},
3818
+ ],
3819
+ }
3820
+
3821
+ step = get_dml_operation(
3822
+ sobject="Test__c",
3823
+ operation=operation,
3824
+ fields=["Birthdate", "IsHappy", "Name"],
3825
+ api_options={},
3826
+ context=context,
3827
+ api=DataApi.REST,
3828
+ volume=1,
3829
+ )
3830
+ # Empty dates (and other fields) should be NULLED in UPSERTs
3831
+ # Booleans become False for backwards-compatibility reasons.
3832
+ json_out = step._record_to_json(["", "", "Bill"])
3833
+ assert json_out == {
3834
+ "Birthdate": None,
3835
+ "IsHappy": False,
3836
+ "Name": "Bill",
3837
+ "attributes": {"type": "Test__c"},
3838
+ }, json_out
3839
+
3840
+
3841
+ @pytest.mark.parametrize(
3842
+ "query_fields, expected",
3843
+ [
3844
+ # Test with simple field names
3845
+ (["Id", "Name", "Email"], ["Id", "Name", "Email"]),
3846
+ # Test with TYPEOF fields (polymorphic fields)
3847
+ (
3848
+ [
3849
+ "Subject",
3850
+ {
3851
+ "Who": [
3852
+ {"Contact": ["Name", "Email"]},
3853
+ {"Lead": ["Name", "Company"]},
3854
+ ]
3855
+ },
3856
+ ],
3857
+ [
3858
+ "Subject",
3859
+ "Who.Contact.Name",
3860
+ "Who.Contact.Email",
3861
+ "Who.Lead.Name",
3862
+ "Who.Lead.Company",
3863
+ ],
3864
+ ),
3865
+ # Test with mixed simple and TYPEOF fields
3866
+ (
3867
+ ["Subject", {"Who": [{"Contact": ["Email"]}]}, "Account.Name"],
3868
+ ["Subject", "Who.Contact.Email", "Account.Name"],
3869
+ ),
3870
+ # Test with an empty list
3871
+ ([], []),
3872
+ ],
3873
+ )
3874
+ def test_extract_flattened_headers(query_fields, expected):
3875
+ result = extract_flattened_headers(query_fields)
3876
+ assert result == expected
3877
+
3878
+
3879
+ @pytest.mark.parametrize(
3880
+ "record, headers, expected",
3881
+ [
3882
+ # Test with simple field matching
3883
+ (
3884
+ {"Id": "001", "Name": "John Doe", "Email": "john@example.com"},
3885
+ ["Id", "Name", "Email"],
3886
+ ["001", "John Doe", "john@example.com"],
3887
+ ),
3888
+ # Test with lookup fields and missing values
3889
+ (
3890
+ {
3891
+ "Who": {
3892
+ "attributes": {"type": "Contact"},
3893
+ "Name": "Jane Doe",
3894
+ "Email": "johndoe@org.com",
3895
+ "Number": 10,
3896
+ }
3897
+ },
3898
+ ["Who.Contact.Name", "Who.Contact.Email", "Who.Contact.Number"],
3899
+ ["Jane Doe", "johndoe@org.com", "10"],
3900
+ ),
3901
+ # Test with non-matching ref_obj type
3902
+ (
3903
+ {"Who": {"attributes": {"type": "Contact"}, "Email": "jane@contact.com"}},
3904
+ ["Who.Lead.Email"],
3905
+ [""],
3906
+ ),
3907
+ # Test with mixed fields and nested lookups
3908
+ (
3909
+ {
3910
+ "Who": {"attributes": {"type": "Lead"}, "Name": "John Doe"},
3911
+ "Email": "john@example.com",
3912
+ },
3913
+ ["Who.Lead.Name", "Who.Lead.Company", "Email"],
3914
+ ["John Doe", "", "john@example.com"],
3915
+ ),
3916
+ # Test with mixed fields and nested lookups
3917
+ (
3918
+ {
3919
+ "Who": {"attributes": {"type": "Lead"}, "Name": "John Doe"},
3920
+ "Email": "john@example.com",
3921
+ },
3922
+ ["What.Account.Name"],
3923
+ [""],
3924
+ ),
3925
+ # Test with empty record
3926
+ ({}, ["Id", "Name"], ["", ""]),
3927
+ ],
3928
+ )
3929
+ def test_flatten_record(record, headers, expected):
3930
+ result = flatten_record(record, headers)
3931
+ assert result == expected
3932
+
3933
+
3934
+ @pytest.mark.parametrize(
3935
+ "priority_fields, fields, expected",
3936
+ [
3937
+ # Test with priority fields matching
3938
+ (
3939
+ {"Id": "Id", "Name": "Name"},
3940
+ ["Id", "Name", "Email"],
3941
+ [HIGH_PRIORITY_VALUE, HIGH_PRIORITY_VALUE, LOW_PRIORITY_VALUE],
3942
+ ),
3943
+ # Test with no priority fields provided
3944
+ (None, ["Id", "Name", "Email"], [1, 1, 1]),
3945
+ # Test with empty priority fields dictionary
3946
+ ({}, ["Id", "Name", "Email"], [1, 1, 1]),
3947
+ # Test with some fields not in priority_fields
3948
+ (
3949
+ {"Id": "Id"},
3950
+ ["Id", "Name", "Email"],
3951
+ [HIGH_PRIORITY_VALUE, LOW_PRIORITY_VALUE, LOW_PRIORITY_VALUE],
3952
+ ),
3953
+ ],
3954
+ )
3955
+ def test_assign_weights(priority_fields, fields, expected):
3956
+ result = assign_weights(priority_fields, fields)
3957
+ assert result == expected