class TestSalesforceToolingTask(unittest.TestCase): def setUp(self): self.api_version = 36.0 self.global_config = BaseGlobalConfig( {"project": { "package": { "api_version": self.api_version } }}) self.project_config = BaseProjectConfig(self.global_config, config={"noyaml": True}) self.project_config.config["project"] = { "package": { "api_version": self.api_version } } self.project_config.config["services"] = { "connectedapp": { "attributes": { "client_id": {} } } } self.keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(self.keychain) self.task_config = TaskConfig() self.org_config = OrgConfig( { "instance_url": "https://example.com", "access_token": "abc123" }, "test") self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version) def test_get_tooling_object(self): task = BaseSalesforceApiTask(self.project_config, self.task_config, self.org_config) task._init_task() obj = task._get_tooling_object("TestObject") url = self.base_tooling_url + "sobjects/TestObject/" self.assertEqual(obj.base_url, url) def test_default_client_name(self): task = BaseSalesforceApiTask(self.project_config, self.task_config, self.org_config) task._init_task() self.assertIn("Sforce-Call-Options", task.sf.headers) self.assertIn("CumulusCI/", task.sf.headers["Sforce-Call-Options"]) def test_connected_app_client_name(self): self.project_config.keychain.set_service( "connectedapp", ServiceConfig({"client_id": "test123"})) task = BaseSalesforceApiTask(self.project_config, self.task_config, self.org_config) task._init_task() self.assertIn("Sforce-Call-Options", task.sf.headers) self.assertNotIn("CumulusCI/", task.sf.headers["Sforce-Call-Options"]) self.assertIn("test123", task.sf.headers["Sforce-Call-Options"])
def _make_task(task_class, task_config): task_config = TaskConfig(task_config) global_config = BaseGlobalConfig() project_config = BaseProjectConfig(global_config, config={"noyaml": True}) keychain = BaseProjectKeychain(project_config, "") project_config.set_keychain(keychain) org_config = DummyOrgConfig( {"instance_url": "https://example.com", "access_token": "abc123"}, "test" ) return task_class(project_config, task_config, org_config)
def _make_task(task_class, task_config): task_config = TaskConfig(task_config) global_config = BaseGlobalConfig() project_config = BaseProjectConfig(global_config, config={"noyaml": True}) keychain = BaseProjectKeychain(project_config, "") project_config.set_keychain(keychain) org_config = DummyOrgConfig( {"instance_url": "https://example.com", "access_token": "abc123"}, "test" ) return task_class(project_config, task_config, org_config)
class TestSalesforceToolingTask(unittest.TestCase): def setUp(self): self.api_version = 36.0 self.global_config = BaseGlobalConfig( {"project": {"package": {"api_version": self.api_version}}} ) self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } self.project_config.config["services"] = { "connectedapp": {"attributes": {"client_id": {}}} } self.keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(self.keychain) self.task_config = TaskConfig() self.org_config = OrgConfig( {"instance_url": "https://example.com", "access_token": "abc123"}, "test" ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) def test_get_tooling_object(self): task = BaseSalesforceApiTask( self.project_config, self.task_config, self.org_config ) task._init_task() obj = task._get_tooling_object("TestObject") url = self.base_tooling_url + "sobjects/TestObject/" self.assertEqual(obj.base_url, url) def test_default_client_name(self): task = BaseSalesforceApiTask( self.project_config, self.task_config, self.org_config ) task._init_task() self.assertIn("Sforce-Call-Options", task.sf.headers) self.assertIn("CumulusCI/", task.sf.headers["Sforce-Call-Options"]) def test_connected_app_client_name(self): self.project_config.keychain.set_service( "connectedapp", ServiceConfig({"client_id": "test123"}) ) task = BaseSalesforceApiTask( self.project_config, self.task_config, self.org_config ) task._init_task() self.assertIn("Sforce-Call-Options", task.sf.headers) self.assertNotIn("CumulusCI/", task.sf.headers["Sforce-Call-Options"]) self.assertIn("test123", task.sf.headers["Sforce-Call-Options"])
def _run_task(self): # Find or create Version if not self.dry_run: product = self._find_product() version = self._find_or_create_version(product) # Check out the specified tag repo_owner = self.project_config.repo_owner repo_name = self.project_config.repo_name gh = self.project_config.get_github_api() repo = gh.repository(repo_owner, repo_name) if self.tag: tag = self.options["tag"] self.commit = repo.tag(repo.ref("tags/" + tag).object.sha).object.sha self.logger.info("Downloading commit {} of {} from GitHub".format( self.commit, repo.full_name)) zf = download_extract_github(gh, repo_owner, repo_name, ref=self.commit) with temporary_dir() as project_dir: zf.extractall(project_dir) project_config = BaseProjectConfig( self.project_config.global_config_obj, repo_info={ "root": project_dir, "owner": repo_owner, "name": repo_name, "url": self.project_config.repo_url, "branch": self.tag or self.commit, "commit": self.commit, }, ) project_config.set_keychain(self.project_config.keychain) # Create each plan for plan_name, plan_config in self.plan_configs.items(): steps = self._freeze_steps(project_config, plan_config) self.logger.debug("Prepared steps:\n" + json.dumps(steps, indent=4)) if not self.dry_run: self._publish_plan(product, version, plan_name, plan_config, steps) # Update version to set is_listed=True if self.publish: self._call_api( "PATCH", "/versions/{}".format(version["id"]), json={"is_listed": True}, ) self.logger.info("Published Version {}".format(version["url"]))
def _make_task(task_class, task_config): task_config = TaskConfig(task_config) universal_config = UniversalConfig() project_config = BaseProjectConfig( universal_config, config={"noyaml": True, "project": {"package": {"api_version": "46.0"}}}, ) keychain = BaseProjectKeychain(project_config, "") project_config.set_keychain(keychain) org_config = DummyOrgConfig( {"instance_url": "https://example.com", "access_token": "abc123"}, "test" ) return task_class(project_config, task_config, org_config)
def _run_task(self): # Find or create Version if not self.dry_run: product = self._find_product() version = self._find_or_create_version(product) # Check out the specified tag repo_owner = self.project_config.repo_owner repo_name = self.project_config.repo_name gh = self.project_config.get_github_api() repo = gh.repository(repo_owner, repo_name) tag = self.options["tag"] commit_sha = repo.tag(repo.ref("tags/" + tag).object.sha).object.sha self.logger.info( "Downloading commit {} of {} from GitHub".format(commit_sha, repo.full_name) ) zf = download_extract_github(gh, repo_owner, repo_name, ref=commit_sha) with temporary_dir() as project_dir: zf.extractall(project_dir) project_config = BaseProjectConfig( self.project_config.global_config_obj, repo_info={ "root": project_dir, "owner": repo_owner, "name": repo_name, "url": self.project_config.repo_url, "branch": tag, "commit": commit_sha, }, ) project_config.set_keychain(self.project_config.keychain) # Create each plan for plan_name, plan_config in self.plan_configs.items(): steps = self._freeze_steps(project_config, plan_config) self.logger.debug("Prepared steps:\n" + json.dumps(steps, indent=4)) if not self.dry_run: self._publish_plan(product, version, plan_name, plan_config, steps) # Update version to set is_listed=True if not self.dry_run: self._call_api( "PATCH", "/versions/{}".format(version["id"]), json={"is_listed": True}, ) self.logger.info("Published Version {}".format(version["url"]))
class TestSFDXBaseTask(unittest.TestCase): """ Tests for the Base Task type """ @classmethod def setUpClass(cls): super(TestSFDXBaseTask, cls).setUpClass() logger = logging.getLogger(cumulusci.core.tasks.__name__) logger.setLevel(logging.DEBUG) cls._task_log_handler = MockLoggingHandler(logging.DEBUG) logger.addHandler(cls._task_log_handler) def setUp(self): self.global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig(self.global_config) self.task_config = TaskConfig() keychain = BaseProjectKeychain(self.project_config, '') app_config = ConnectedAppOAuthConfig() keychain.set_connected_app(app_config) self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages def test_base_task(self): """ The command is prefixed w/ sfdx """ self.task_config.config['options'] = { 'command': 'force:org --help', } task = SFDXBaseTask(self.project_config, self.task_config) try: task() except CommandException: pass self.assertEqual('sfdx force:org --help', task.options['command']) @patch('cumulusci.tasks.sfdx.SFDXOrgTask._update_credentials', MagicMock(return_value=None)) @patch('cumulusci.tasks.command.Command._run_task', MagicMock(return_value=None)) def test_keychain_org_creds(self): """ Keychain org creds are passed by env var """ self.task_config.config['options'] = { 'command': 'force:org --help', } access_token = '00d123' org_config = OrgConfig({ 'access_token': access_token, 'instance_url': 'https://test.salesforce.com' },'test') task = SFDXOrgTask( self.project_config, self.task_config, org_config ) try: task() except CommandException: pass self.assertIn('SFDX_INSTANCE_URL', task._get_env()) self.assertIn('SFDX_USERNAME', task._get_env()) self.assertIn(access_token, task._get_env()['SFDX_USERNAME']) def test_scratch_org_username(self): """ Scratch Org credentials are passed by -u flag """ self.task_config.config['options'] = { 'command': 'force:org --help', } org_config = OrgConfig({ 'access_token': 'test access token', 'instance_url': 'test instance url', }, 'test') task = SFDXOrgTask( self.project_config, self.task_config, org_config ) try: task() except CommandException: pass env = task._get_env() print env self.assertEquals(env.get('SFDX_USERNAME'), 'test access token') self.assertEquals(env.get('SFDX_INSTANCE_URL'), 'test instance url')
class TestRunBatchApex(unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": {"api_version": self.api_version}} ) self.task_config = TaskConfig() self.task_config.config["options"] = { "class_name": "ADDR_Seasonal_BATCH", "poll_interval": 1, } self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) def _get_query_resp(self): return { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "AsyncApexJob", "records": [ { "attributes": { "type": "AsyncApexJob", "url": "/services/data/v43.0/tooling/sobjects/AsyncApexJob/707L0000014nnPHIAY", }, "Id": "707L0000014nnPHIAY", "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v43.0/tooling/sobjects/ApexClass/01pL000000109ndIAA", }, "Name": "ADDR_Seasonal_BATCH", }, "Status": "Completed", "ExtendedStatus": None, "TotalJobItems": 1, "JobItemsProcessed": 1, "NumberOfErrors": 0, "CreatedDate": "2018-08-07T16:00:56.000+0000", "CompletedDate": "2018-08-07T16:01:57.000+0000", } ], } def _get_url_and_task(self): task = BatchApexWait(self.project_config, self.task_config, self.org_config) url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27+ORDER+BY+CreatedDate+DESC+LIMIT+1" ) return task, url @responses.activate def test_run_batch_apex_status_fail(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["NumberOfErrors"] = 1 response["records"][0]["ExtendedStatus"] = "Bad Status" responses.add(responses.GET, url, json=response) with self.assertRaises(SalesforceException) as cm: task() err = cm.exception self.assertEqual(err.args[0], "Bad Status") @responses.activate def test_run_batch_apex_status_ok(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_batch_apex_calc_delta(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() self.assertEqual(task.delta, 61)
def _run_task(self): # Find or create Version product = self._find_product() if not self.dry_run: version = self._find_or_create_version(product) if self.labels_path and "slug" in product: self._publish_labels(product["slug"]) # Check out the specified tag repo_owner = self.project_config.repo_owner repo_name = self.project_config.repo_name gh = self.project_config.get_github_api() repo = gh.repository(repo_owner, repo_name) if self.tag: tag = self.options["tag"] self.commit = repo.tag(repo.ref("tags/" + tag).object.sha).object.sha self.logger.info( "Downloading commit {} of {} from GitHub".format( self.commit, repo.full_name ) ) zf = download_extract_github(gh, repo_owner, repo_name, ref=self.commit) with temporary_dir() as project_dir: zf.extractall(project_dir) project_config = BaseProjectConfig( self.project_config.universal_config_obj, repo_info={ "root": project_dir, "owner": repo_owner, "name": repo_name, "url": self.project_config.repo_url, "branch": self.tag or self.commit, "commit": self.commit, }, ) project_config.set_keychain(self.project_config.keychain) # Create each plan for plan_name, plan_config in self.plan_configs.items(): self._add_labels( plan_config, f"plan:{plan_name}", { "title": "title of installation plan", "preflight_message": "shown before user starts installation (markdown)", "preflight_message_additional": "shown before user starts installation (markdown)", "post_install_message": "shown after successful installation (markdown)", "post_install_message_additional": "shown after successful installation (markdown)", "error_message": "shown after failed installation (markdown)", }, ) checks = plan_config.get("checks") or [] for check in checks: self._add_label( "checks", check.get("message"), "shown if validation fails" ) steps = self._freeze_steps(project_config, plan_config) self.logger.debug("Prepared steps:\n" + json.dumps(steps, indent=4)) for step in steps: # avoid separate labels for installing each package if step["name"].startswith("Install "): self._add_label( "steps", "Install {product} {version}", "title of installation step", ) else: self._add_label( "steps", step["name"], "title of installation step" ) self._add_label( "steps", step.get("description"), "description of installation step", ) for check in step["task_config"].get("checks", []): self._add_label( "checks", check.get("message"), "shown if validation fails" ) if not self.dry_run: self._publish_plan(product, version, plan_name, plan_config, steps) # Update version to set is_listed=True if self.publish: self._call_api( "PATCH", "/versions/{}".format(version["id"]), json={"is_listed": True}, ) self.logger.info("Published Version {}".format(version["url"])) # Save labels self._save_labels()
class TestRunApexTests(unittest.TestCase): def setUp(self): self.api_version = 38.0 self.global_config = BaseGlobalConfig( {'project': {'api_version': self.api_version}}) self.task_config = TaskConfig() self.task_config.config['options'] = { 'junit_output': 'results_junit.xml', 'poll_interval': 1, 'test_name_match': '%_TEST', } self.project_config = BaseProjectConfig(self.global_config) self.project_config.config['project'] = {'package': { 'api_version': self.api_version}} keychain = BaseProjectKeychain(self.project_config, '') self.project_config.set_keychain(keychain) self.org_config = OrgConfig({ 'id': 'foo/1', 'instance_url': 'example.com', 'access_token': 'abc123', }, 'test') self.base_tooling_url = 'https://{}/services/data/v{}/tooling/'.format( self.org_config.instance_url, self.api_version) def _mock_apex_class_query(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Name+' + 'FROM+ApexClass+WHERE+NamespacePrefix+%3D+null' + '+AND+%28Name+LIKE+%27%25_TEST%27%29') expected_response = { 'done': True, 'records': [{'Id': 1, 'Name': 'TestClass_TEST'}], 'totalSize': 1, } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_get_test_results(self): url = (self.base_tooling_url + 'query/?q=%0ASELECT+Id%2CApexClassId%2CTestTimestamp%2C%0A+++++++Message%2CMethodName%2COutcome%2C%0A+++++++RunTime%2CStackTrace%2C%0A+++++++%28SELECT+%0A++++++++++Id%2CCallouts%2CAsyncCalls%2CDmlRows%2CEmail%2C%0A++++++++++LimitContext%2CLimitExceptions%2CMobilePush%2C%0A++++++++++QueryRows%2CSosl%2CCpu%2CDml%2CSoql+%0A++++++++FROM+ApexTestResults%29+%0AFROM+ApexTestResult+%0AWHERE+AsyncApexJobId%3D%27JOB_ID1234567%27%0A') expected_response = { 'done': True, 'records': [{ "attributes": { "type": "ApexTestResult", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResult/07M41000009gbT3EAI" }, "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v40.0/tooling/sobjects/ApexClass/01p4100000Fu4Z0AAJ" }, "Name": "EP_TaskDependency_TEST" }, "ApexClassId": 1, "ApexLogId": 1, "TestTimestamp": "2017-07-18T20:36:04.000+0000", "Id": "07M41000009gbT3EAI", "Message": "Test Passed", "MethodName": "TestMethod", "Outcome": "Pass", "QueueItem": { "attributes": { "type": "ApexTestQueueItem", "url": "/services/data/v40.0/tooling/sobjects/ApexTestQueueItem/70941000000q7VsAAI" }, "Status": "Completed", "ExtendedStatus": "(4/4)" }, "RunTime": 1707, "StackTrace": "1. ParentFunction\n2. ChildFunction", "Status": 'Completed', 'Name': 'TestClass_TEST', "ApexTestResults": { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "ApexTestResultLimits", "records": [{ "attributes": { "type": "ApexTestResultLimits", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResultLimits/05n41000002Y7OQAA0" }, "Id": "05n41000002Y7OQAA0", "Callouts": 0, "AsyncCalls": 0, "DmlRows": 5, "Email": 0, "LimitContext": "SYNC", "LimitExceptions": None, "MobilePush": 0, "QueryRows": 20, "Sosl": 0, "Cpu": 471, "Dml": 4, "Soql": 5 }] } }]} responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_tests_complete(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Status%2C+' + 'ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27' + 'JOB_ID1234567%27') expected_response = { 'done': True, 'records': [{'Status': 'Completed'}], } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_run_tests(self): url = self.base_tooling_url + 'runTestsAsynchronous' expected_response = 'JOB_ID1234567' responses.add(responses.POST, url, json=expected_response) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests( self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 4)
class TestRunCustomSettingsWait(MockLoggerMixin, unittest.TestCase): def setUp(self): self.api_version = 42.0 self.universal_config = UniversalConfig( {"project": {"api_version": self.api_version}} ) self.task_config = TaskConfig() self.project_config = BaseProjectConfig( self.universal_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = DummyOrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_normal_url = "{}/services/data/v{}/query/".format( self.org_config.instance_url, self.api_version ) self.task_log = self._task_log_handler.messages def _get_query_resp(self): return { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "Customizable_Rollup_Setings__c", "records": [ { "attributes": { "type": "Customizable_Rollup_Setings__c", "url": "/services/data/v47.0/sobjects/Customizable_Rollup_Setings__c/707L0000014nnPHIAY", }, "Id": "707L0000014nnPHIAY", "SetupOwnerId": "00Dxxxxxxxxxxxx", "Customizable_Rollups_Enabled__c": True, "Rollups_Account_Batch_Size__c": 200, } ], } def _get_url_and_task(self): task = CustomSettingValueWait( self.project_config, self.task_config, self.org_config ) url = self.base_normal_url return task, url @responses.activate def test_run_custom_settings_wait_match_bool(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Customizable_Rollups_Enabled__c", "value": True, "poll_interval": 1, } # simulate finding no settings record and then finding one with the expected value task, url = self._get_url_and_task() responses.add( responses.GET, url, json={"totalSize": 0, "done": True, "records": []} ) response = self._get_query_resp() response["records"][0]["Customizable_Rollups_Enabled__c"] = True responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_match_bool_changed_case(self): self.task_config.config["options"] = { "object": "CUSTOMIZABLE_Rollup_Setings__c", "field": "CUSTOMIZABLE_Rollups_enabled__C", "value": True, "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Customizable_Rollups_Enabled__c"] = True responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_match_int(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Rollups_Account_Batch_Size__c", "value": 200, "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Rollups_Account_Batch_Size__c"] = 200 responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_match_str(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Rollups_Account_Batch_Size__c", "value": "asdf", "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Rollups_Account_Batch_Size__c"] = "asdf" responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_not_settings_object(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Rollups_Account_Batch_Size__c", "value": 200, "poll_interval": 1, } task, url = self._get_url_and_task() responses.add( responses.GET, url, status=400, json=[ { "message": "\nSELECT SetupOwnerId FROM npe5__Affiliation__c\n ^\nERROR at Row:1:Column:8\nNo such column 'SetupOwnerId' on entity 'npe5__Affiliation__c'. If you are attempting to use a custom field, be sure to append the '__c' after the custom field name. Please reference your WSDL or the describe call for the appropriate names.", "errorCode": "INVALID_FIELD", } ], ) with self.assertRaises(TaskOptionsError) as e: task() assert "supported" in str(e.exception) def test_apply_namespace__managed(self): self.project_config.config["project"]["package"]["namespace"] = "ns" self.task_config.config["options"] = { "object": "%%%NAMESPACE%%%Test__c", "field": "Field__c", "value": "x", "managed": True, "namespaced": True, } task, url = self._get_url_and_task() task.object_name = "%%%NAMESPACE%%%Test__c" task.field_name = "%%%NAMESPACE%%%Field__c" task._apply_namespace() assert task.object_name == "ns__Test__c" assert task.field_name == "ns__Field__c"
class TestRunApexTests(unittest.TestCase): def setUp(self): self.api_version = 38.0 self.global_config = BaseGlobalConfig( {'project': {'api_version': self.api_version}}) self.task_config = TaskConfig() self.task_config.config['options'] = { 'junit_output': 'results_junit.xml', 'poll_interval': 1, 'test_name_match': '%_TEST', } self.project_config = BaseProjectConfig(self.global_config) self.project_config.config['project'] = {'package': { 'api_version': self.api_version}} keychain = BaseProjectKeychain(self.project_config, '') app_config = ConnectedAppOAuthConfig() keychain.set_connected_app(app_config) self.project_config.set_keychain(keychain) self.org_config = OrgConfig({ 'id': 'foo/1', 'instance_url': 'example.com', 'access_token': 'abc123', }) self.base_tooling_url = 'https://{}/services/data/v{}/tooling/'.format( self.org_config.instance_url, self.api_version) def _mock_apex_class_query(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Name+' + 'FROM+ApexClass+WHERE+NamespacePrefix+%3D+null' + '+AND+%28Name+LIKE+%27%25_TEST%27%29') expected_response = { 'done': True, 'records': [{'Id': 1, 'Name': 'TestClass_TEST'}], 'totalSize': 1, } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_get_test_results(self): url = (self.base_tooling_url + 'query/?q=SELECT+StackTrace%2C+' + 'Message%2C+ApexLogId%2C+AsyncApexJobId%2C+MethodName%2C+' + 'Outcome%2C+ApexClassId%2C+TestTimestamp+FROM+ApexTestResult+' + 'WHERE+AsyncApexJobId+%3D+%27JOB_ID1234567%27') expected_response = { 'done': True, 'records': [{ 'ApexClassId': 1, 'ApexLogId': 1, 'Id': 1, 'Message': 'Test passed', 'MethodName': 'TestMethod', 'Name': 'TestClass_TEST', 'Outcome': 'Pass', 'StackTrace': '1. ParentFunction\n2. ChildFunction', 'Status': 'Completed', }], } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_tests_complete(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Status%2C+' + 'ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27' + 'JOB_ID1234567%27') expected_response = { 'done': True, 'records': [{'Status': 'Completed'}], } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_run_tests(self): url = self.base_tooling_url + 'runTestsAsynchronous' expected_response = 'JOB_ID1234567' responses.add(responses.POST, url, json=expected_response) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests( self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 4)
class TestMrbelvederePublish(unittest.TestCase): def setUp(self): self.project_config = BaseProjectConfig(BaseGlobalConfig(), get_base_config()) self.project_config.config["project"]["package"]["namespace"] = "npsp" self.project_config.config["project"]["dependencies"] = [ {"namespace": "nochangedep", "version": "1.0"}, {"namespace": "changedep", "version": "1.1"}, ] keychain = BaseProjectKeychain(self.project_config, "") keychain.set_service( "mrbelvedere", ServiceConfig({"base_url": "http://mrbelvedere", "api_key": "1234"}), ) self.project_config.set_keychain(keychain) self.task_config = TaskConfig({"options": {"tag": "beta/1.0-Beta_2"}}) @responses.activate def test_run_task(self): responses.add( responses.GET, "http://mrbelvedere/npsp/dependencies/beta", body=json.dumps( [ {"namespace": "npsp", "number": "1.0 (Beta 1)"}, {"namespace": "changedep", "number": "1.0"}, {"namespace": "nochangedep", "number": "1.0"}, {"namespace": "other", "number": "1.0"}, ] ), ) responses.add( responses.POST, "http://mrbelvedere/npsp/dependencies/beta", status=200 ) task = MrbelvederePublish(self.project_config, self.task_config) task() result = json.loads(responses.calls[-1].request.body) self.assertEqual( [ {"namespace": "changedep", "number": "1.1"}, {"namespace": "npsp", "number": "1.0 (Beta 2)"}, ], result, ) @responses.activate def test_run_task__http_error(self): responses.add( responses.POST, "http://mrbelvedere/npsp/dependencies/beta", status=500 ) task = MrbelvederePublish(self.project_config, self.task_config) task._get_current_dependencies = mock.Mock( return_value=[{"namespace": "npsp", "number": "1.0 (Beta 1)"}] ) with self.assertRaises(MrbelvedereError): task() def test_clean_dependencies(self): task = MrbelvederePublish(self.project_config, self.task_config) result = task._clean_dependencies( [ {"github": "https://github.com/SFDO-Tooling/CumulusCI-Test"}, {"namespace": "foo", "version": "1.0"}, {"namespace": "foo", "version": "2.0", "dependencies": []}, ] ) self.assertEqual([{"namespace": "foo", "number": "2.0"}], result) def test_clean_dependencies__no_deps(self): task = MrbelvederePublish(self.project_config, self.task_config) result = task._clean_dependencies([]) self.assertEqual([], result)
class TestRunBatchApex(MockLoggerMixin, unittest.TestCase): def setUp(self): self.api_version = 42.0 self.universal_config = UniversalConfig( {"project": {"api_version": self.api_version}} ) self.task_config = TaskConfig() self.task_config.config["options"] = { "class_name": "ADDR_Seasonal_BATCH", "poll_interval": 1, } self.project_config = BaseProjectConfig( self.universal_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) self.task_log = self._task_log_handler.messages def _get_query_resp(self): return { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "AsyncApexJob", "records": [ { "attributes": { "type": "AsyncApexJob", "url": "/services/data/v43.0/tooling/sobjects/AsyncApexJob/707L0000014nnPHIAY", }, "Id": "707L0000014nnPHIAY", "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v43.0/tooling/sobjects/ApexClass/01pL000000109ndIAA", }, "Name": "ADDR_Seasonal_BATCH", }, "Status": "Completed", "ExtendedStatus": None, "TotalJobItems": 1, "JobItemsProcessed": 1, "NumberOfErrors": 0, "CreatedDate": "2018-08-07T16:00:56.000+0000", "CompletedDate": "2018-08-07T16:01:57.000+0000", } ], } def _update_job_result(self, response: dict, result_dict: dict): "Extend the result from _get_query_resp with additional batch records" template_result = response["records"][-1] # use the last result as a template assert isinstance(template_result, dict) new_result = {**template_result, **result_dict} # copy with variations old_subjob_results = [ # set completed for all old subjob results {**record, "Status": "Completed"} for record in response["records"] ] result_list = [ new_result ] + old_subjob_results # prepend new result because SOQL is order by DESC return {**response, "records": result_list} def _get_url_and_task(self): task = BatchApexWait(self.project_config, self.task_config, self.org_config) url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27++++ORDER+BY+CreatedDate+DESC++LIMIT+1+" ) return task, url @responses.activate def test_run_batch_apex_status_fail(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["NumberOfErrors"] = 1 response["records"][0]["ExtendedStatus"] = "Bad Status" responses.add(responses.GET, url, json=response) with self.assertRaises(SalesforceException) as cm: task() err = cm.exception self.assertIn("Bad Status", err.args[0]) @responses.activate def test_run_batch_apex_number_mismatch(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["JobItemsProcessed"] = 1 response["records"][0]["TotalJobItems"] = 3 responses.add(responses.GET, url, json=response) task() self.assertIn("The final record counts do not add up.", self.task_log["info"]) @responses.activate def test_run_batch_apex_status_ok(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_batch_apex_calc_elapsed_time(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() self.assertEqual(task.elapsed_time(task.subjobs), 61) @responses.activate def test_run_batch_apex_status_aborted(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Status"] = "Aborted" response["records"][0]["JobItemsProcessed"] = 1 response["records"][0]["TotalJobItems"] = 3 responses.add(responses.GET, url, json=response) with self.assertRaises(SalesforceException) as e: task() assert "aborted" in str(e.exception) @responses.activate def test_run_batch_apex_status_failed(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Status"] = "Failed" response["records"][0]["JobItemsProcessed"] = 1 response["records"][0]["TotalJobItems"] = 3 responses.add(responses.GET, url, json=response) self.task_log["info"] = [] with self.assertRaises(SalesforceException) as e: task() assert "failure" in str(e.exception) @responses.activate def test_chained_subjobs(self): "Test subjobs that kick off a successor before they complete" task, url = self._get_url_and_task() url2 = ( url.split("?")[0] + "?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27++AND+CreatedDate+%3E%3D+2018-08-07T16%3A00%3A00Z++ORDER+BY+CreatedDate+DESC++" ) # batch 1 response = self._get_query_resp() batch_record = response["records"][0] batch_record.update( { "JobItemsProcessed": 1, "TotalJobItems": 3, "NumberOfErrors": 0, "Status": "Processing", "CreatedDate": "2018-08-07T16:00:00.000+0000", } ) responses.add(responses.GET, url, json=response) # batch 2: 1 error response = self._update_job_result( response, {"Id": "Id2", "NumberOfErrors": 1, "Status": "Processing"} ) responses.add(responses.GET, url2, json=response) # batch 3: found another error response = self._update_job_result( response, {"Id": "Id2", "NumberOfErrors": 2, "Status": "Processing"} ) responses.add(responses.GET, url2, json=response) # batch 4: Complete, no errors in this sub-batch response = self._update_job_result( response, { "NumberOfErrors": 0, "Id": "Id4", "Status": "Completed", "CompletedDate": "2018-08-07T16:10:00.000+0000", # 10 minutes passed }, ) responses.add(responses.GET, url2, json=response.copy()) with self.assertRaises(SalesforceException) as e: task() assert len(task.subjobs) == 4 summary = task.summarize_subjobs(task.subjobs) assert not summary["Success"] assert not summary["CountsAddUp"] assert summary["ElapsedTime"] == 10 * 60 assert summary["JobItemsProcessed"] == 4 assert summary["TotalJobItems"] == 12 assert summary["NumberOfErrors"] == 3 assert "batch errors" in str(e.exception) @responses.activate def test_chained_subjobs_beginning(self): "Test the first subjob that kicks off a successor before they complete" task, url = self._get_url_and_task() url2 = ( url.split("?")[0] + "?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27++AND+CreatedDate+%3E%3D+2018-08-07T16%3A00%3A00Z++ORDER+BY+CreatedDate+DESC++" ) # batch 1 response = self._get_query_resp() responses.add(responses.GET, url2, json=response) batch_record = response["records"][0] batch_record.update( { "JobItemsProcessed": 1, "TotalJobItems": 3, "NumberOfErrors": 0, "Status": "Preparing", "CreatedDate": "2018-08-07T16:00:00.000+0000", "CompletedDate": None, } ) real_poll_action = task._poll_action counter = 0 def mock_poll_action(): nonlocal counter counter += 1 if counter == 1: task.poll_complete = False return real_poll_action() else: rc = real_poll_action() task.poll_complete = True return rc task._poll_action = mock_poll_action responses.add(responses.GET, url, json=response) task() assert counter == 2 assert task.poll_complete summary = task.summarize_subjobs(task.subjobs) assert not summary["NumberOfErrors"] @responses.activate def test_chained_subjobs_halfway(self): "Test part-way through a series of subjobs that kick off a successor before they complete" task, url = self._get_url_and_task() url2 = ( url.split("?")[0] + "?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27++AND+CreatedDate+%3E%3D+2018-08-07T16%3A00%3A00Z++ORDER+BY+CreatedDate+DESC++" ) # batch 1 response = self._get_query_resp() responses.add(responses.GET, url2, json=response) batch_record = response["records"][0] batch_record.update( { "JobItemsProcessed": 1, "TotalJobItems": 3, "NumberOfErrors": 0, "Status": "Preparing", "CreatedDate": "2018-08-07T16:00:00.000+0000", "CompletedDate": "2018-08-07T16:05:00.000+0000", } ) # batch 2: 1 error response = self._update_job_result( response, {"Id": "Id2", "NumberOfErrors": 1, "CompletedDate": None} ) responses.add(responses.GET, url2, json=response) real_poll_action = task._poll_action counter = 0 def mock_poll_action(): nonlocal counter counter += 1 if counter == 1: task.poll_complete = False return real_poll_action() else: rc = real_poll_action() task.poll_complete = True return rc task._poll_action = mock_poll_action responses.add(responses.GET, url, json=response) task() assert counter == 2 assert task.poll_complete summary = task.summarize_subjobs(task.subjobs) assert not summary["NumberOfErrors"] @responses.activate def test_job_not_found(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"] = [] responses.add(responses.GET, url, json=response) with self.assertRaises(SalesforceException) as e: task() assert "found" in str(e.exception)
class TestAnonymousApexTask(unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": { "api_version": self.api_version }}) self.tmpdir = tempfile.mkdtemp(dir=".") apex_path = os.path.join(self.tmpdir, "test.apex") with open(apex_path, "w") as f: f.write('System.debug("from file")') self.task_config = TaskConfig() self.task_config.config["options"] = { "path": apex_path, "apex": 'system.debug("Hello World!")', "namespaced": True, } self.project_config = BaseProjectConfig(self.global_config, config={"noyaml": True}) self.project_config.config = { "project": { "package": { "namespace": "abc", "api_version": self.api_version } } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version) def tearDown(self): shutil.rmtree(self.tmpdir) def _get_url_and_task(self): task = AnonymousApexTask(self.project_config, self.task_config, self.org_config) url = self.base_tooling_url + "executeAnonymous" return task, url def test_validate_options(self): task_config = TaskConfig({}) with self.assertRaises(TaskOptionsError): AnonymousApexTask(self.project_config, task_config, self.org_config) def test_run_from_path_outside_repo(self): task_config = TaskConfig({"options": {"path": "/"}}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task() def test_run_path_not_found(self): task_config = TaskConfig({"options": {"path": "bogus"}}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task() def test_prepare_apex(self): task = AnonymousApexTask(self.project_config, self.task_config, self.org_config) before = "String %%%NAMESPACE%%%str = 'foo';" expected = "String abc__str = 'foo';" self.assertEqual(expected, task._prepare_apex(before)) @responses.activate def test_run_anonymous_apex_success(self): task, url = self._get_url_and_task() resp = {"compiled": True, "success": True} responses.add(responses.GET, url, status=200, json=resp) task() @responses.activate def test_run_string_only(self): task_config = TaskConfig( {"options": { "apex": 'System.debug("test");' }}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) url = self.base_tooling_url + "executeAnonymous" responses.add(responses.GET, url, status=200, json={ "compiled": True, "success": True }) task() @responses.activate def test_run_anonymous_apex_status_fail(self): task, url = self._get_url_and_task() responses.add(responses.GET, url, status=418, body="I'm a teapot") with self.assertRaises(SalesforceGeneralError) as cm: task() err = cm.exception self.assertEqual(str(err), "Error Code 418. Response content: I'm a teapot") self.assertTrue(err.url.startswith(url)) self.assertEqual(err.status, 418) self.assertEqual(err.content, "I'm a teapot") @responses.activate def test_run_anonymous_apex_compile_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." resp = { "compiled": False, "compileProblem": problem, "success": False, "line": 1, "column": 13, "exceptionMessage": "", "exceptionStackTrace": "", "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexCompilationException) as cm: task() err = cm.exception self.assertEqual(err.args[0], 1) self.assertEqual(err.args[1], problem) @responses.activate def test_run_anonymous_apex_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." trace = "Line 0, Column 99" resp = { "compiled": True, "compileProblem": "", "success": False, "line": 1, "column": 13, "exceptionMessage": problem, "exceptionStackTrace": trace, "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexException) as cm: task() err = cm.exception self.assertEqual(err.args[0], problem) self.assertEqual(err.args[1], trace)
class TestSFDXBaseTask(MockLoggerMixin, unittest.TestCase): """ Tests for the Base Task type """ def setUp(self): self.universal_config = UniversalConfig() self.project_config = BaseProjectConfig(self.universal_config, config={"noyaml": True}) self.task_config = TaskConfig() keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages def test_base_task(self): """ The command is prefixed w/ sfdx """ self.task_config.config["options"] = { "command": "force:org", "extra": "--help" } task = SFDXBaseTask(self.project_config, self.task_config) try: task() except CommandException: pass self.assertEqual("force:org", task.options["command"]) self.assertEqual("sfdx force:org --help", task._get_command()) @patch( "cumulusci.tasks.sfdx.SFDXOrgTask._update_credentials", MagicMock(return_value=None), ) @patch("cumulusci.tasks.command.Command._run_task", MagicMock(return_value=None)) def test_keychain_org_creds(self): """ Keychain org creds are passed by env var """ self.task_config.config["options"] = {"command": "force:org --help"} access_token = "00d123" org_config = OrgConfig( { "access_token": access_token, "instance_url": "https://test.salesforce.com", }, "test", ) task = SFDXOrgTask(self.project_config, self.task_config, org_config) try: task() except CommandException: pass self.assertIn("SFDX_INSTANCE_URL", task._get_env()) self.assertIn("SFDX_DEFAULTUSERNAME", task._get_env()) self.assertIn(access_token, task._get_env()["SFDX_DEFAULTUSERNAME"]) def test_scratch_org_username(self): """ Scratch Org credentials are passed by -u flag """ self.task_config.config["options"] = {"command": "force:org --help"} org_config = ScratchOrgConfig({"username": "******"}, "test") task = SFDXOrgTask(self.project_config, self.task_config, org_config) self.assertIn("-u [email protected]", task._get_command())
class TestCreateConnectedApp(MockLoggerMixin, unittest.TestCase): """ Tests for the CreateConnectedApp task """ def setUp(self): self.global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages self.base_command = "sfdx force:mdapi:deploy --wait {}".format( CreateConnectedApp.deploy_wait ) self.label = "Test_Label" self.username = "******" self.email = "TestUser@Email" self.task_config = TaskConfig( { "options": { "label": self.label, "username": self.username, "email": self.email, } } ) def test_init_options(self): """ Passed options are correctly initialized """ self.task_config.config["options"]["connect"] = True self.task_config.config["options"]["overwrite"] = True task = CreateConnectedApp(self.project_config, self.task_config) self.assertEqual( task.options["command"], "{} -u {}".format(self.base_command, self.username) ) self.assertEqual(task.options["label"], self.label) self.assertEqual(task.options["username"], self.username) self.assertEqual(task.options["email"], self.email) self.assertIs(task.options["connect"], True) self.assertIs(task.options["overwrite"], True) def test_init_options_invalid_label(self): """ Non-alphanumeric + _ label raises TaskOptionsError """ self.task_config.config["options"]["label"] = "Test Label" with pytest.raises(TaskOptionsError, match="^label value must contain only"): CreateConnectedApp(self.project_config, self.task_config) def test_init_options_email_default(self): """ email option defaults to email from github service """ del self.task_config.config["options"]["email"] self.project_config.config["services"] = { "github": {"attributes": {"email": {}}} } self.project_config.keychain.set_service( "github", ServiceConfig({"email": self.email}), True ) task = CreateConnectedApp(self.project_config, self.task_config) self.assertEqual(task.options["email"], self.email) def test_init_options_email_not_found(self): """ TaskOptionsError is raised if no email provided and no github service exists """ del self.task_config.config["options"]["email"] self.project_config.config["services"] = {"github": {"attributes": {}}} with pytest.raises(TaskOptionsError, match="github"): CreateConnectedApp(self.project_config, self.task_config) @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username") def test_init_options_default_username(self, set_mock): """ Not passing username calls _get_default_username """ del self.task_config.config["options"]["username"] try: CreateConnectedApp(self.project_config, self.task_config) except Exception: pass set_mock.assert_called_once() @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._run_command") def test_set_default_username(self, run_command_mock): """ _set_default_username calls _run_command """ task = CreateConnectedApp(self.project_config, self.task_config) run_command_mock.side_effect = lambda **kw: kw["output_handler"]( b'{"result":[{"value":"username"}]}' ) task._set_default_username() run_command_mock.assert_called_once() self.assertEqual( self.task_log["info"], ["Getting username for the default devhub from sfdx"] ) def test_process_json_output(self): """ _process_json_output returns valid json """ task = CreateConnectedApp(self.project_config, self.task_config) output = task._process_json_output('{"foo":"bar"}') self.assertEqual(output, {"foo": "bar"}) def test_process_json_output_invalid(self): """ _process_json_output with invalid input logs output and raises JSONDecodeError """ task = CreateConnectedApp(self.project_config, self.task_config) with pytest.raises(JSONDecodeError): task._process_json_output("invalid") self.assertEqual( self.task_log["error"], ["Failed to parse json from output: invalid"] ) @mock.patch( "cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username", MagicMock(return_value=None), ) def test_process_devhub_output(self): """ username is parsed from json response """ del self.task_config.config["options"]["username"] task = CreateConnectedApp(self.project_config, self.task_config) task._process_devhub_output('{"result":[{"value":"' + self.username + '"}]}') self.assertEqual(task.options.get("username"), self.username) @mock.patch( "cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username", MagicMock(return_value=None), ) def test_process_devhub_output_not_configured(self): """ TaskOptionsError is raised if no username provided and no default found """ del self.task_config.config["options"]["username"] task = CreateConnectedApp(self.project_config, self.task_config) with pytest.raises(TaskOptionsError, match="^No sfdx config found"): task._process_devhub_output('{"result":[{}]}') def test_generate_id_and_secret(self): """ client_id and client_secret are generated correctly """ task = CreateConnectedApp(self.project_config, self.task_config) task._generate_id_and_secret() self.assertEqual(len(task.client_id), task.client_id_length) self.assertEqual(len(task.client_secret), task.client_secret_length) self.assertNotEqual(re.match(r"^\w+$", task.client_id), None) self.assertNotEqual(re.match(r"^\w+$", task.client_secret), None) def test_build_package(self): """ tempdir is populated with connected app and package.xml """ task = CreateConnectedApp(self.project_config, self.task_config) with temporary_dir() as tempdir: task.tempdir = tempdir connected_app_path = os.path.join( task.tempdir, "connectedApps", "{}.connectedApp".format(self.label) ) task._build_package() self.assertTrue(os.path.isdir(os.path.join(task.tempdir, "connectedApps"))) self.assertTrue(os.path.isfile(os.path.join(task.tempdir, "package.xml"))) self.assertTrue(os.path.isfile(connected_app_path)) with open(connected_app_path, "r") as f: connected_app = f.read() self.assertTrue("<label>{}<".format(self.label) in connected_app) self.assertTrue("<contactEmail>{}<".format(self.email) in connected_app) self.assertTrue( "<consumerKey>{}<".format(task.client_id) in connected_app ) self.assertTrue( "<consumerSecret>{}<".format(task.client_secret) in connected_app ) def test_connect_service(self): """ connected app gets added to the keychain connected_app service """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } task = CreateConnectedApp(self.project_config, self.task_config) task._connect_service() connected_app = self.project_config.keychain.get_service("connected_app") self.assertEqual(connected_app.callback_url, "http://*****:*****@mock.patch("cumulusci.tasks.sfdx.SFDXBaseTask._run_task") def test_run_task(self, run_task_mock): """ _run_task formats command, calls SFDXBaseTask._run_task, and does not connect service by default """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } task = CreateConnectedApp(self.project_config, self.task_config) task._run_task() run_task_mock.assert_called_once() self.assertFalse(os.path.isdir(task.tempdir)) self.assertEqual( task.options["command"], self.base_command + " -u {} -d {}".format(self.username, task.tempdir), ) with pytest.raises(ServiceNotConfigured): self.project_config.keychain.get_service("connected_app") @mock.patch("cumulusci.tasks.sfdx.SFDXBaseTask._run_task") @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._connect_service") def test_run_task_connect(self, run_task_mock, connect_service_mock): """ _run_task calls _connect_service if connect option is True """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } self.task_config.config["options"]["connect"] = True task = CreateConnectedApp(self.project_config, self.task_config) task._run_task() run_task_mock.assert_called_once() connect_service_mock.assert_called_once()
class TestCreateConnectedApp(MockLoggerMixin, unittest.TestCase): """ Tests for the CreateConnectedApp task """ def setUp(self): self.global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages self.base_command = "sfdx force:mdapi:deploy --wait {}".format( CreateConnectedApp.deploy_wait ) self.label = "Test_Label" self.username = "******" self.email = "TestUser@Email" self.task_config = TaskConfig( { "options": { "label": self.label, "username": self.username, "email": self.email, } } ) def test_init_options(self): """ Passed options are correctly initialized """ self.task_config.config["options"]["connect"] = True self.task_config.config["options"]["overwrite"] = True task = CreateConnectedApp(self.project_config, self.task_config) self.assertEqual( task.options["command"], "{} -u {}".format(self.base_command, self.username) ) self.assertEqual(task.options["label"], self.label) self.assertEqual(task.options["username"], self.username) self.assertEqual(task.options["email"], self.email) self.assertIs(task.options["connect"], True) self.assertIs(task.options["overwrite"], True) def test_init_options_invalid_label(self): """ Non-alphanumeric + _ label raises TaskOptionsError """ self.task_config.config["options"]["label"] = "Test Label" with pytest.raises(TaskOptionsError, match="^label value must contain only"): CreateConnectedApp(self.project_config, self.task_config) def test_init_options_email_default(self): """ email option defaults to email from github service """ del self.task_config.config["options"]["email"] self.project_config.config["services"] = { "github": {"attributes": {"email": {}}} } self.project_config.keychain.set_service( "github", ServiceConfig({"email": self.email}), True ) task = CreateConnectedApp(self.project_config, self.task_config) self.assertEqual(task.options["email"], self.email) def test_init_options_email_not_found(self): """ TaskOptionsError is raised if no email provided and no github service exists """ del self.task_config.config["options"]["email"] self.project_config.config["services"] = {"github": {"attributes": {}}} with pytest.raises(TaskOptionsError, match="github"): CreateConnectedApp(self.project_config, self.task_config) @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username") def test_init_options_default_username(self, set_mock): """ Not passing username calls _get_default_username """ del self.task_config.config["options"]["username"] try: CreateConnectedApp(self.project_config, self.task_config) except Exception: pass set_mock.assert_called_once() @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._run_command") def test_set_default_username(self, run_command_mock): """ _set_default_username calls _run_command """ task = CreateConnectedApp(self.project_config, self.task_config) run_command_mock.side_effect = lambda **kw: kw["output_handler"]( b'{"result":[{"value":"username"}]}' ) task._set_default_username() run_command_mock.assert_called_once() self.assertEqual( self.task_log["info"], ["Getting username for the default devhub from sfdx"] ) def test_process_json_output(self): """ _process_json_output returns valid json """ task = CreateConnectedApp(self.project_config, self.task_config) output = task._process_json_output('{"foo":"bar"}') self.assertEqual(output, {"foo": "bar"}) def test_process_json_output_invalid(self): """ _process_json_output with invalid input logs output and raises JSONDecodeError """ task = CreateConnectedApp(self.project_config, self.task_config) with pytest.raises(JSONDecodeError): task._process_json_output("invalid") self.assertEqual( self.task_log["error"], ["Failed to parse json from output: invalid"] ) @mock.patch( "cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username", MagicMock(return_value=None), ) def test_process_devhub_output(self): """ username is parsed from json response """ del self.task_config.config["options"]["username"] task = CreateConnectedApp(self.project_config, self.task_config) task._process_devhub_output('{"result":[{"value":"' + self.username + '"}]}') self.assertEqual(task.options.get("username"), self.username) @mock.patch( "cumulusci.tasks.connectedapp.CreateConnectedApp._set_default_username", MagicMock(return_value=None), ) def test_process_devhub_output_not_configured(self): """ TaskOptionsError is raised if no username provided and no default found """ del self.task_config.config["options"]["username"] task = CreateConnectedApp(self.project_config, self.task_config) with pytest.raises(TaskOptionsError, match="^No sfdx config found"): task._process_devhub_output('{"result":[{}]}') def test_generate_id_and_secret(self): """ client_id and client_secret are generated correctly """ task = CreateConnectedApp(self.project_config, self.task_config) task._generate_id_and_secret() self.assertEqual(len(task.client_id), task.client_id_length) self.assertEqual(len(task.client_secret), task.client_secret_length) self.assertNotEqual(re.match(r"^\w+$", task.client_id), None) self.assertNotEqual(re.match(r"^\w+$", task.client_secret), None) def test_build_package(self): """ tempdir is populated with connected app and package.xml """ task = CreateConnectedApp(self.project_config, self.task_config) with temporary_dir() as tempdir: task.tempdir = tempdir connected_app_path = os.path.join( task.tempdir, "connectedApps", "{}.connectedApp".format(self.label) ) task._build_package() self.assertTrue(os.path.isdir(os.path.join(task.tempdir, "connectedApps"))) self.assertTrue(os.path.isfile(os.path.join(task.tempdir, "package.xml"))) self.assertTrue(os.path.isfile(connected_app_path)) with open(connected_app_path, "r") as f: connected_app = f.read() self.assertTrue("<label>{}<".format(self.label) in connected_app) self.assertTrue("<contactEmail>{}<".format(self.email) in connected_app) self.assertTrue( "<consumerKey>{}<".format(task.client_id) in connected_app ) self.assertTrue( "<consumerSecret>{}<".format(task.client_secret) in connected_app ) def test_connect_service(self): """ connected app gets added to the keychain connected_app service """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } task = CreateConnectedApp(self.project_config, self.task_config) task._connect_service() connected_app = self.project_config.keychain.get_service("connected_app") self.assertEqual(connected_app.callback_url, "http://*****:*****@mock.patch("cumulusci.tasks.sfdx.SFDXBaseTask._run_task") def test_run_task(self, run_task_mock): """ _run_task formats command, calls SFDXBaseTask._run_task, and does not connect service by default """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } task = CreateConnectedApp(self.project_config, self.task_config) task._run_task() run_task_mock.assert_called_once() self.assertFalse(os.path.isdir(task.tempdir)) self.assertEqual( task.options["command"], self.base_command + " -u {} -d {}".format(self.username, task.tempdir), ) connected_app = self.project_config.keychain.get_service("connected_app") assert connected_app is DEFAULT_CONNECTED_APP @mock.patch("cumulusci.tasks.sfdx.SFDXBaseTask._run_task") @mock.patch("cumulusci.tasks.connectedapp.CreateConnectedApp._connect_service") def test_run_task_connect(self, run_task_mock, connect_service_mock): """ _run_task calls _connect_service if connect option is True """ self.project_config.config["services"] = { "connected_app": { "attributes": {"callback_url": {}, "client_id": {}, "client_secret": {}} } } self.task_config.config["options"]["connect"] = True task = CreateConnectedApp(self.project_config, self.task_config) task._run_task() run_task_mock.assert_called_once() connect_service_mock.assert_called_once()
class TestRunAnonApex(unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": { "api_version": self.api_version }}) self.task_config = TaskConfig() self.task_config.config["options"] = { "apex": 'system.debug("Hello World!")' } self.project_config = BaseProjectConfig(self.global_config) self.project_config.config["project"] = { "package": { "api_version": self.api_version } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "example.com", "access_token": "abc123" }, "test", ) self.base_tooling_url = "https://{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version) def _get_url_and_task(self): task = AnonymousApexTask(self.project_config, self.task_config, self.org_config) url = task.tooling.base_url + "executeAnonymous" return task, url @responses.activate def test_run_anonymous_apex_status_fail(self): task, url = self._get_url_and_task() responses.add(responses.GET, url, status=418, body="I'm a teapot") with self.assertRaises(SalesforceGeneralError) as cm: task() err = cm.exception self.assertEqual(str(err), "Error Code 418. Response content: I'm a teapot") self.assertTrue(err.url.startswith(url)) self.assertEqual(err.status, 418) self.assertEqual(err.content, "I'm a teapot") @responses.activate def test_run_anonymous_apex_compile_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." resp = { "compiled": False, "compileProblem": problem, "success": False, "line": 1, "column": 13, "exceptionMessage": "", "exceptionStackTrace": "", "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexCompilationException) as cm: task() err = cm.exception self.assertEqual(err[0], 1) self.assertEqual(err[1], problem) @responses.activate def test_run_anonymous_apex_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." trace = "Line 0, Column 99" resp = { "compiled": True, "compileProblem": "", "success": False, "line": 1, "column": 13, "exceptionMessage": problem, "exceptionStackTrace": trace, "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexException) as cm: task() err = cm.exception self.assertEqual(err[0], problem) self.assertEqual(err[1], trace)
class TestRunCustomSettingsWait(MockLoggerMixin, unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": { "api_version": self.api_version }}) self.task_config = TaskConfig() self.project_config = BaseProjectConfig(self.global_config, config={"noyaml": True}) self.project_config.config["project"] = { "package": { "api_version": self.api_version } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_normal_url = "{}/services/data/v{}/query/".format( self.org_config.instance_url, self.api_version) self.task_log = self._task_log_handler.messages def _get_query_resp(self): return { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "Customizable_Rollup_Setings__c", "records": [{ "attributes": { "type": "Customizable_Rollup_Setings__c", "url": "/services/data/v47.0/sobjects/Customizable_Rollup_Setings__c/707L0000014nnPHIAY", }, "Id": "707L0000014nnPHIAY", "SetupOwnerId": "00Dxxxxxxxxxxxx", "Customizable_Rollups_Enabled__c": True, "Rollups_Account_Batch_Size__c": 200, }], } def _get_url_and_task(self): task = CustomSettingValueWait(self.project_config, self.task_config, self.org_config) url = self.base_normal_url return task, url @responses.activate def test_run_custom_settings_wait_match_bool(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Customizable_Rollups_Enabled__c", "value": True, "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Customizable_Rollups_Enabled__c"] = True responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_match_int(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Rollups_Account_Batch_Size__c", "value": 200, "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["Rollups_Account_Batch_Size__c"] = 200 responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_custom_settings_wait_bad_object(self): self.task_config.config["options"] = { "object": "Customizable_Rollup_Setings__c", "field": "Rollups_Account_Batch_Size__c", "value": 200, "poll_interval": 1, } task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["SetupOwnerId"] = "00X" responses.add(responses.GET, url, json=response) # task() with self.assertRaises(SalesforceException) as e: task() assert "found" in str(e.exception)
class TestMrbelvederePublish(unittest.TestCase): def setUp(self): global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig(global_config, config=global_config.config) self.project_config.config["project"]["package"]["namespace"] = "npsp" self.project_config.config["project"]["dependencies"] = [ { "namespace": "nochangedep", "version": "1.0" }, { "namespace": "changedep", "version": "1.1" }, ] keychain = BaseProjectKeychain(self.project_config, "") keychain.set_service( "mrbelvedere", ServiceConfig({ "base_url": "http://mrbelvedere", "api_key": "1234" }), ) self.project_config.set_keychain(keychain) self.task_config = TaskConfig({"options": {"tag": "beta/1.0-Beta_2"}}) @responses.activate def test_run_task(self): responses.add( responses.GET, "http://mrbelvedere/npsp/dependencies/beta", body=json.dumps([ { "namespace": "npsp", "number": "1.0 (Beta 1)" }, { "namespace": "changedep", "number": "1.0" }, { "namespace": "nochangedep", "number": "1.0" }, { "namespace": "other", "number": "1.0" }, ]), ) responses.add(responses.POST, "http://mrbelvedere/npsp/dependencies/beta", status=200) task = MrbelvederePublish(self.project_config, self.task_config) task() result = json.loads(responses.calls[-1].request.body) self.assertEqual( [ { "namespace": "changedep", "number": "1.1" }, { "namespace": "npsp", "number": "1.0 (Beta 2)" }, ], result, ) @responses.activate def test_run_task__http_error(self): responses.add(responses.POST, "http://mrbelvedere/npsp/dependencies/beta", status=500) task = MrbelvederePublish(self.project_config, self.task_config) task._get_current_dependencies = mock.Mock( return_value=[{ "namespace": "npsp", "number": "1.0 (Beta 1)" }]) with self.assertRaises(MrbelvedereError): task() def test_clean_dependencies(self): task = MrbelvederePublish(self.project_config, self.task_config) result = task._clean_dependencies([ { "github": "https://github.com/SFDO-Tooling/CumulusCI-Test" }, { "namespace": "foo", "version": "1.0" }, { "namespace": "foo", "version": "2.0", "dependencies": [] }, ]) self.assertEqual([{"namespace": "foo", "number": "2.0"}], result) def test_clean_dependencies__no_deps(self): task = MrbelvederePublish(self.project_config, self.task_config) result = task._clean_dependencies([]) self.assertEqual([], result)
class TestRunApexTests(unittest.TestCase): def setUp(self): self.api_version = 38.0 self.global_config = BaseGlobalConfig( {"project": {"api_version": self.api_version}} ) self.task_config = TaskConfig() self.task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", } self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) def _mock_apex_class_query(self): url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+Name+" + "FROM+ApexClass+WHERE+NamespacePrefix+%3D+null" + "+AND+%28Name+LIKE+%27%25_TEST%27%29" ) expected_response = { "done": True, "records": [{"Id": 1, "Name": "TestClass_TEST"}], "totalSize": 1, } responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_get_test_results(self, outcome="Pass"): url = ( self.base_tooling_url + "query/?q=%0ASELECT+Id%2CApexClassId%2CTestTimestamp%2C%0A+++++++Message%2CMethodName%2COutcome%2C%0A+++++++RunTime%2CStackTrace%2C%0A+++++++%28SELECT+%0A++++++++++Id%2CCallouts%2CAsyncCalls%2CDmlRows%2CEmail%2C%0A++++++++++LimitContext%2CLimitExceptions%2CMobilePush%2C%0A++++++++++QueryRows%2CSosl%2CCpu%2CDml%2CSoql+%0A++++++++FROM+ApexTestResults%29+%0AFROM+ApexTestResult+%0AWHERE+AsyncApexJobId%3D%27JOB_ID1234567%27%0A" ) expected_response = { "done": True, "records": [ { "attributes": { "type": "ApexTestResult", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResult/07M41000009gbT3EAI", }, "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v40.0/tooling/sobjects/ApexClass/01p4100000Fu4Z0AAJ", }, "Name": "EP_TaskDependency_TEST", }, "ApexClassId": 1, "ApexLogId": 1, "TestTimestamp": "2017-07-18T20:36:04.000+0000", "Id": "07M41000009gbT3EAI", "Message": "Test Passed", "MethodName": "TestMethod", "Outcome": outcome, "QueueItem": { "attributes": { "type": "ApexTestQueueItem", "url": "/services/data/v40.0/tooling/sobjects/ApexTestQueueItem/70941000000q7VsAAI", }, "Status": "Completed", "ExtendedStatus": "(4/4)", }, "RunTime": 1707, "StackTrace": "1. ParentFunction\n2. ChildFunction", "Status": "Completed", "Name": "TestClass_TEST", "ApexTestResults": { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "ApexTestResultLimits", "records": [ { "attributes": { "type": "ApexTestResultLimits", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResultLimits/05n41000002Y7OQAA0", }, "Id": "05n41000002Y7OQAA0", "Callouts": 0, "AsyncCalls": 0, "DmlRows": 5, "Email": 0, "LimitContext": "SYNC", "LimitExceptions": None, "MobilePush": 0, "QueryRows": 20, "Sosl": 0, "Cpu": 471, "Dml": 4, "Soql": 5, } ], }, } ], } responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_tests_complete(self): url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+Status%2C+" + "ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27" + "JOB_ID1234567%27" ) expected_response = {"done": True, "records": [{"Status": "Completed"}]} responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_run_tests(self, success=True): url = self.base_tooling_url + "runTestsAsynchronous" if success: expected_response = "JOB_ID1234567" responses.add(responses.POST, url, json=expected_response) else: responses.add(responses.POST, url, status=http.client.SERVICE_UNAVAILABLE) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests(self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 4) @responses.activate def test_run_task__server_error(self): self._mock_apex_class_query() self._mock_run_tests(success=False) task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(SalesforceGeneralError): task() @responses.activate def test_run_task__failed(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results("Fail") task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(ApexTestException): task() def test_get_namespace_filter__managed(self): task_config = TaskConfig({"options": {"managed": True, "namespace": "testns"}}) task = RunApexTests(self.project_config, task_config, self.org_config) namespace = task._get_namespace_filter() self.assertEqual("'testns'", namespace) def test_get_namespace_filter__managed_no_namespace(self): task_config = TaskConfig({"options": {"managed": True}}) task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): namespace = task._get_namespace_filter() def test_get_test_class_query__exclude(self): task_config = TaskConfig( {"options": {"test_name_match": "%_TEST", "test_name_exclude": "EXCL"}} ) task = RunApexTests(self.project_config, task_config, self.org_config) query = task._get_test_class_query() self.assertEqual( "SELECT Id, Name FROM ApexClass WHERE NamespacePrefix = null " "AND (Name LIKE '%_TEST') AND (NOT Name LIKE 'EXCL')", query, ) def test_run_task__no_tests(self): task = RunApexTests(self.project_config, self.task_config, self.org_config) task._get_test_classes = MagicMock(return_value={"totalSize": 0}) task() self.assertIsNone(task.result)
def _run_task(self): tag = self.options["tag"] repo_owner = self.project_config.repo_owner repo_name = self.project_config.repo_name gh = self.project_config.get_github_api() repo = gh.repository(repo_owner, repo_name) commit_sha = repo.tag(repo.ref("tags/" + tag).object.sha).object.sha self.logger.info("Downloading commit {} of {}/{} from GitHub".format( commit_sha, repo_owner, repo_name)) zf = download_extract_github(gh, repo_owner, repo_name, ref=commit_sha) with temporary_dir() as project_dir: zf.extractall(project_dir) project_config = BaseProjectConfig( self.project_config.global_config_obj, repo_info={ "root": project_dir, "owner": repo_owner, "name": repo_name, "url": self.project_config.repo_url, "branch": tag, "commit": commit_sha, }, ) project_config.set_keychain(self.project_config.keychain) steps = self._freeze_steps(project_config) self.logger.debug("Publishing steps:\n" + json.dumps(steps, indent=4)) # create version (not listed yet) product_url = self.base_url + "/products/{}".format( self.options["product_id"]) label = self.project_config.get_version_for_tag(tag) version = self._call_api( "POST", "/versions", json={ "product": product_url, "label": label, "description": self.options.get("description", ""), "is_production": True, "commit_ish": self.project_config.repo_commit, "is_listed": False, }, ) self.logger.info("Created {}".format(version["url"])) # create plan plan_template_id = self.options.get("plan_template_id") plan_template_url = (self.base_url + "/plantemplates/{}".format(plan_template_id) if plan_template_id else None) plan = self._call_api( "POST", "/plans", json={ "plan_template": plan_template_url, "post_install_message_additional": self.options.get("post_install_message_additional", ""), "preflight_message_additional": self.options.get("preflight_message_additional", ""), "steps": steps, "tier": self.options["tier"], "title": self.options["title"], "version": version["url"], }, ) self.logger.info("Created Plan {}".format(plan["url"])) # create plan slug planslug = self._call_api( "POST", "/planslug", json={ "parent": plan["url"], "slug": self.options["slug"] }, ) self.logger.info("Created PlanSlug {}".format(planslug["url"])) # update version to set is_listed=True self._call_api("PATCH", "/versions/{}".format(version["id"]), json={"is_listed": True}) self.logger.info("Published Version {}".format(version["url"]))
class TestSFDXBaseTask(unittest.TestCase): """ Tests for the Base Task type """ @classmethod def setUpClass(cls): super(TestSFDXBaseTask, cls).setUpClass() logger = logging.getLogger(cumulusci.core.tasks.__name__) logger.setLevel(logging.DEBUG) cls._task_log_handler = MockLoggingHandler(logging.DEBUG) logger.addHandler(cls._task_log_handler) def setUp(self): self.global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig(self.global_config) self.task_config = TaskConfig() keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages def test_base_task(self): """ The command is prefixed w/ sfdx """ self.task_config.config["options"] = {"command": "force:org --help"} task = SFDXBaseTask(self.project_config, self.task_config) try: task() except CommandException: pass self.assertEqual("sfdx force:org --help", task.options["command"]) @patch( "cumulusci.tasks.sfdx.SFDXOrgTask._update_credentials", MagicMock(return_value=None), ) @patch("cumulusci.tasks.command.Command._run_task", MagicMock(return_value=None)) def test_keychain_org_creds(self): """ Keychain org creds are passed by env var """ self.task_config.config["options"] = {"command": "force:org --help"} access_token = "00d123" org_config = OrgConfig( { "access_token": access_token, "instance_url": "https://test.salesforce.com", }, "test", ) task = SFDXOrgTask(self.project_config, self.task_config, org_config) try: task() except CommandException: pass self.assertIn("SFDX_INSTANCE_URL", task._get_env()) self.assertIn("SFDX_USERNAME", task._get_env()) self.assertIn(access_token, task._get_env()["SFDX_USERNAME"]) def test_scratch_org_username(self): """ Scratch Org credentials are passed by -u flag """ self.task_config.config["options"] = {"command": "force:org --help"} org_config = OrgConfig( {"access_token": "test access token", "instance_url": "test instance url"}, "test", ) task = SFDXOrgTask(self.project_config, self.task_config, org_config) try: task() except CommandException: pass env = task._get_env() print(env) self.assertEquals(env.get("SFDX_USERNAME"), "test access token") self.assertEquals(env.get("SFDX_INSTANCE_URL"), "test instance url")
class TestGitHubSource(unittest.TestCase, MockUtil): def setUp(self): self.repo_api_url = "https://api.github.com/repos/TestOwner/TestRepo" universal_config = UniversalConfig() self.project_config = BaseProjectConfig(universal_config) self.project_config.set_keychain( BaseProjectKeychain(self.project_config, None)) self.project_config.keychain.set_service( "github", ServiceConfig({ "username": "******", "password": "******", "email": "*****@*****.**", }), ) @responses.activate def test_resolve__default(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (tag_sha)>" ) @responses.activate def test_resolve__default_no_release(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", status=404, ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/heads/main", json=self._get_expected_ref("heads/main", "abcdef"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) assert (repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ main (abcdef)>") @responses.activate def test_resolve__commit(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "commit": "abcdef" }, ) assert repr( source) == "<GitHubSource GitHub: TestOwner/TestRepo @ abcdef>" @responses.activate def test_resolve__ref(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/main", json=self._get_expected_ref("main", "abcdef"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "ref": "main" }, ) assert (repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ main (abcdef)>") @responses.activate def test_resolve__branch(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/heads/main", json=self._get_expected_ref("main", "abcdef"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "branch": "main" }, ) assert (repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ main (abcdef)>") @responses.activate def test_resolve__tag(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "abcdef"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "tag": "release/1.0", }, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (abcdef)>" ) @responses.activate def test_resolve__latest_release(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "release": "latest", }, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (tag_sha)>" ) @responses.activate def test_resolve__latest_beta(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases", json=[self._get_expected_release("beta/1.0-Beta_1")], ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/beta/1.0-Beta_1", json=self._get_expected_tag_ref("beta/1.0-Beta_1", "tag_sha"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "release": "latest_beta", }, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/beta/1.0-Beta_1 (tag_sha)>" ) @responses.activate def test_resolve__previous_release(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases", json=[ self._get_expected_release("release/2.0"), self._get_expected_release("beta/1.0-Beta_1", prerelease=True), self._get_expected_release("release/1.0"), ], ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "release": "previous", }, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (tag_sha)>" ) @responses.activate def test_resolve__release_not_found(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", status=404, ) with pytest.raises(DependencyResolutionError): GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "release": "latest", }, ) @responses.activate def test_resolve__bogus_release(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) with pytest.raises(DependencyResolutionError): GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "release": "bogus", }, ) @responses.activate def test_fetch(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) f = io.BytesIO() zf = zipfile.ZipFile(f, "w") zfi = zipfile.ZipInfo("toplevel/") zf.writestr(zfi, "") zf.writestr( "toplevel/cumulusci.yml", yaml.dump({ "project": { "package": { "name_managed": "Test Product", "namespace": "ns" } } }), ) zf.close() responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/zipball/tag_sha", body=f.getvalue(), content_type="application/zip", ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) with temporary_dir() as d: project_config = source.fetch() assert isinstance(project_config, BaseProjectConfig) assert project_config.repo_root == os.path.join( os.path.realpath(d), ".cci", "projects", "TestRepo", "tag_sha") @responses.activate @mock.patch("cumulusci.core.source.github.download_extract_github") def test_fetch__cleans_up_after_failed_extract(self, download_extract_github): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) # Set up a fake IOError while extracting the zipball download_extract_github.return_value = mock.Mock(extractall=mock.Mock( side_effect=IOError)) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) with temporary_dir(): with pytest.raises(IOError): source.fetch() assert not pathlib.Path(".cci", "projects", "TestRepo", "tag_sha").exists() @responses.activate def test_hash(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) assert hash(source) == hash( ("https://github.com/TestOwner/TestRepo", "tag_sha")) @responses.activate def test_frozenspec(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"}) assert source.frozenspec == { "github": "https://github.com/TestOwner/TestRepo", "commit": "tag_sha", "description": "tags/release/1.0", }
class TestGitHubSource(unittest.TestCase, MockUtil): def setUp(self): self.repo_api_url = "https://api.github.com/repos/TestOwner/TestRepo" global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig(global_config) self.project_config.set_keychain(BaseProjectKeychain(self.project_config, None)) self.project_config.keychain.set_service( "github", ServiceConfig( { "username": "******", "password": "******", "email": "*****@*****.**", } ), ) @responses.activate def test_resolve__latest(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"} ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (tag_sha)>" ) @responses.activate def test_resolve__no_release(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", status=404, ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/heads/master", json=self._get_expected_ref("heads/master", "abcdef"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"} ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ master (abcdef)>" ) @responses.activate def test_resolve__commit(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git", "commit": "abcdef"}, ) assert repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ abcdef>" @responses.activate def test_resolve__ref(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/master", json=self._get_expected_ref("master", "abcdef"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git", "ref": "master"}, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ master (abcdef)>" ) @responses.activate def test_resolve__branch(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/heads/master", json=self._get_expected_ref("master", "abcdef"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git", "branch": "master"}, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ master (abcdef)>" ) @responses.activate def test_fetch(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) f = io.BytesIO() zf = zipfile.ZipFile(f, "w") zfi = zipfile.ZipInfo("toplevel/") zf.writestr(zfi, "") zf.writestr( "toplevel/cumulusci.yml", yaml.dump( { "project": { "package": {"name_managed": "Test Product", "namespace": "ns"} } } ), ) zf.close() responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/zipball/tag_sha", body=f.getvalue(), content_type="application/zip", ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"} ) with temporary_dir() as d: project_config = source.fetch() assert isinstance(project_config, BaseProjectConfig) assert project_config.repo_root == os.path.join( os.path.realpath(d), ".cci", "projects", "TestRepo", "tag_sha" ) @responses.activate def test_resolve__tag(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "abcdef"), ) source = GitHubSource( self.project_config, { "github": "https://github.com/TestOwner/TestRepo.git", "tag": "release/1.0", }, ) assert ( repr(source) == "<GitHubSource GitHub: TestOwner/TestRepo @ tags/release/1.0 (abcdef)>" ) @responses.activate def test_hash(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"} ) assert hash(source) == hash( ("https://github.com/TestOwner/TestRepo", "tag_sha") ) @responses.activate def test_frozenspec(self): responses.add( method=responses.GET, url=self.repo_api_url, json=self._get_expected_repo(owner="TestOwner", name="TestRepo"), ) responses.add( "GET", "https://api.github.com/repos/TestOwner/TestRepo/releases/latest", json=self._get_expected_release("release/1.0"), ) responses.add( "GET", f"https://api.github.com/repos/TestOwner/TestRepo/git/refs/tags/release/1.0", json=self._get_expected_tag_ref("release/1.0", "tag_sha"), ) source = GitHubSource( self.project_config, {"github": "https://github.com/TestOwner/TestRepo.git"} ) assert source.frozenspec == { "github": "https://github.com/TestOwner/TestRepo", "commit": "tag_sha", "description": "tags/release/1.0", }
class TestRunApexTests(MockLoggerMixin, unittest.TestCase): def setUp(self): self._task_log_handler.reset() self.task_log = self._task_log_handler.messages self.api_version = 38.0 self.universal_config = UniversalConfig( {"project": {"api_version": self.api_version}} ) self.task_config = TaskConfig() self.task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", } self.project_config = BaseProjectConfig( self.universal_config, config={"noyaml": True} ) self.project_config.config["project"] = { "package": {"api_version": self.api_version} } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) def _mock_apex_class_query(self, name="TestClass_TEST", namespace=None): namespace_param = "null" if namespace is None else f"%27{namespace}%27" url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+Name+" + f"FROM+ApexClass+WHERE+NamespacePrefix+%3D+{namespace_param}" + "+AND+%28Name+LIKE+%27%25_TEST%27%29" ) expected_response = { "done": True, "records": [{"Id": 1, "Name": name}], "totalSize": 1, } responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _get_mock_test_query_results(self, methodnames, outcomes, messages): record_base = { "attributes": { "type": "ApexTestResult", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResult/07M41000009gbT3EAI", }, "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v40.0/tooling/sobjects/ApexClass/01p4100000Fu4Z0AAJ", }, "Name": "EP_TaskDependency_TEST", }, "ApexClassId": 1, "ApexLogId": 1, "TestTimestamp": "2017-07-18T20:36:04.000+0000", "Id": "07M41000009gbT3EAI", "Message": None, "MethodName": None, "Outcome": None, "QueueItem": { "attributes": { "type": "ApexTestQueueItem", "url": "/services/data/v40.0/tooling/sobjects/ApexTestQueueItem/70941000000q7VsAAI", }, "Status": "Completed", "ExtendedStatus": "(4/4)", }, "RunTime": 1707, "StackTrace": "1. ParentFunction\n2. ChildFunction", "Status": "Completed", "Name": "TestClass_TEST", "ApexTestResults": { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "ApexTestResultLimits", "records": [ { "attributes": { "type": "ApexTestResultLimits", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResultLimits/05n41000002Y7OQAA0", }, "Id": "05n41000002Y7OQAA0", "Callouts": 0, "AsyncCalls": 0, "DmlRows": 5, "Email": 0, "LimitContext": "SYNC", "LimitExceptions": None, "MobilePush": 0, "QueryRows": 20, "Sosl": 0, "Cpu": 471, "Dml": 4, "Soql": 5, } ], }, } return_value = {"done": True, "records": []} for (method_name, outcome, message) in zip(methodnames, outcomes, messages): this_result = deepcopy(record_base) this_result["Message"] = message this_result["Outcome"] = outcome this_result["MethodName"] = method_name return_value["records"].append(this_result) return return_value def _get_mock_test_query_url(self, job_id): return ( self.base_tooling_url + "query/?q=%0ASELECT+Id%2CApexClassId%2CTestTimestamp%2C%0A+++++++Message%2CMethodName%2COutcome%2C%0A+++++++RunTime%2CStackTrace%2C%0A+++++++%28SELECT%0A++++++++++Id%2CCallouts%2CAsyncCalls%2CDmlRows%2CEmail%2C%0A++++++++++LimitContext%2CLimitExceptions%2CMobilePush%2C%0A++++++++++QueryRows%2CSosl%2CCpu%2CDml%2CSoql%0A++++++++FROM+ApexTestResults%29%0AFROM+ApexTestResult%0AWHERE+AsyncApexJobId%3D%27{}%27%0A".format( job_id ) ) def _get_mock_testqueueitem_status_query_url(self, job_id): return ( self.base_tooling_url + f"query/?q=SELECT+Id%2C+Status%2C+ExtendedStatus%2C+ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27{job_id}%27+AND+Status+%3D+%27Failed%27" ) def _mock_get_test_results( self, outcome="Pass", message="Test Passed", job_id="JOB_ID1234567" ): url = self._get_mock_test_query_url(job_id) expected_response = self._get_mock_test_query_results( ["TestMethod"], [outcome], [message] ) responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_get_test_results_multiple( self, method_names, outcomes, messages, job_id="JOB_ID1234567" ): url = self._get_mock_test_query_url(job_id) expected_response = self._get_mock_test_query_results( method_names, outcomes, messages ) responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_get_failed_test_classes(self, job_id="JOB_ID1234567"): url = self._get_mock_testqueueitem_status_query_url(job_id) responses.add( responses.GET, url, match_querystring=True, json={"totalSize": 0, "records": [], "done": True}, ) def _mock_get_failed_test_classes_failure(self, job_id="JOB_ID1234567"): url = self._get_mock_testqueueitem_status_query_url(job_id) responses.add( responses.GET, url, match_querystring=True, json={ "totalSize": 1, "records": [ { "Id": "0000000000000000", "ApexClassId": 1, "Status": "Failed", "ExtendedStatus": "Double-plus ungood", } ], "done": True, }, ) def _mock_get_symboltable(self): url = ( self.base_tooling_url + "query/?q=SELECT+SymbolTable+FROM+ApexClass+WHERE+Name%3D%27TestClass_TEST%27" ) responses.add( responses.GET, url, json={ "records": [ { "SymbolTable": { "methods": [ {"name": "test1", "annotations": [{"name": "isTest"}]} ] } } ] }, ) def _mock_get_symboltable_failure(self): url = ( self.base_tooling_url + "query/?q=SELECT+SymbolTable+FROM+ApexClass+WHERE+Name%3D%27TestClass_TEST%27" ) responses.add(responses.GET, url, json={"records": []}) def _mock_tests_complete(self, job_id="JOB_ID1234567"): url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+Status%2C+" + "ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27" + "{}%27".format(job_id) ) expected_response = { "done": True, "totalSize": 1, "records": [{"Status": "Completed"}], } responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_tests_processing(self, job_id="JOB_ID1234567"): url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+Status%2C+" + "ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27" + "{}%27".format(job_id) ) expected_response = { "done": True, "totalSize": 1, "records": [{"Status": "Processing", "ApexClassId": 1}], } responses.add( responses.GET, url, match_querystring=True, json=expected_response ) def _mock_run_tests(self, success=True, body="JOB_ID1234567"): url = self.base_tooling_url + "runTestsAsynchronous" if success: responses.add(responses.POST, url, json=body) else: responses.add(responses.POST, url, status=http.client.SERVICE_UNAVAILABLE) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests(self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 5) @responses.activate def test_run_task__server_error(self): self._mock_apex_class_query() self._mock_run_tests(success=False) task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(SalesforceGeneralError): task() @responses.activate def test_run_task__failed(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results("Fail") task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(ApexTestException): task() @responses.activate def test_run_task__failed_class_level(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes_failure() self._mock_tests_complete() self._mock_get_test_results() self._mock_get_symboltable() task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(ApexTestException): task() @responses.activate def test_run_task__failed_class_level_no_symboltable(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes_failure() self._mock_tests_complete() self._mock_get_test_results() self._mock_get_symboltable_failure() task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(CumulusCIException): task() @responses.activate def test_run_task__failed_class_level_no_symboltable__spring20_managed(self): self._mock_apex_class_query(name="ns__Test_TEST", namespace="ns") self._mock_run_tests() self._mock_get_failed_test_classes_failure() self._mock_tests_complete() self._mock_get_test_results() self._mock_get_symboltable_failure() task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "managed": True, "namespace": "ns", } task = RunApexTests(self.project_config, task_config, self.org_config) task._get_test_methods_for_class = Mock() task() task._get_test_methods_for_class.assert_not_called() @responses.activate def test_run_task__retry_tests(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_run_tests(body="JOBID_9999") self._mock_get_failed_test_classes() self._mock_get_failed_test_classes(job_id="JOBID_9999") self._mock_tests_complete() self._mock_tests_complete(job_id="JOBID_9999") self._mock_get_test_results("Fail", "UNABLE_TO_LOCK_ROW") self._mock_get_test_results(job_id="JOBID_9999") task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) task() self.assertEqual(len(responses.calls), 9) @responses.activate def test_run_task__retry_tests_with_retry_always(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_run_tests(body="JOBID_9999") self._mock_run_tests(body="JOBID_9990") self._mock_get_failed_test_classes() self._mock_get_failed_test_classes(job_id="JOBID_9999") self._mock_get_failed_test_classes(job_id="JOBID_9990") self._mock_tests_complete() self._mock_tests_complete(job_id="JOBID_9999") self._mock_tests_complete(job_id="JOBID_9990") self._mock_get_test_results_multiple( ["TestOne", "TestTwo"], ["Fail", "Fail"], ["UNABLE_TO_LOCK_ROW", "LimitException"], ) self._mock_get_test_results_multiple( ["TestOne"], ["Pass"], [""], job_id="JOBID_9999" ) self._mock_get_test_results_multiple( ["TestTwo"], ["Fail"], ["LimitException"], job_id="JOBID_9990" ) task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], "retry_always": True, } task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(ApexTestException): task() @responses.activate def test_run_task__retry_tests_fails(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_run_tests(body="JOBID_9999") self._mock_get_failed_test_classes() self._mock_get_failed_test_classes(job_id="JOBID_9999") self._mock_tests_complete() self._mock_tests_complete(job_id="JOBID_9999") self._mock_get_test_results("Fail", "UNABLE_TO_LOCK_ROW") self._mock_get_test_results("Fail", "DUPLICATES_DETECTED", job_id="JOBID_9999") task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(ApexTestException): task() @responses.activate def test_run_task__processing(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_processing() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests(self.project_config, self.task_config, self.org_config) task() log = self._task_log_handler.messages assert "Completed: 0 Processing: 1 (TestClass_TEST) Queued: 0" in log["info"] @responses.activate def test_run_task__not_verbose(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_processing() self._mock_get_failed_test_classes() # this returns all passes self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests(self.project_config, self.task_config, self.org_config) task() log = self._task_log_handler.messages assert "Class: TestClass_TEST" not in log["info"] @responses.activate def test_run_task__verbose(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes_failure() self._mock_tests_complete() self._mock_get_test_results() self._mock_get_symboltable() task_config = TaskConfig() task_config.config["options"] = { "verbose": True, "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", } task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(CumulusCIException): task() log = self._task_log_handler.messages assert "Class: TestClass_TEST" in log["info"] @responses.activate def test_run_task__no_code_coverage(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results() task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", } task = RunApexTests(self.project_config, task_config, self.org_config) task._check_code_coverage = Mock() task() task._check_code_coverage.assert_not_called() @responses.activate def test_run_task__checks_code_coverage(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results() task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "required_org_code_coverage_percent": "90", } org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) org_config._installed_packages = {"TEST": StrictVersion("1.2.3")} task = RunApexTests(self.project_config, task_config, org_config) task._check_code_coverage = Mock() task() task._check_code_coverage.assert_called_once() def test_code_coverage_integer(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "required_org_code_coverage_percent": 90, } org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) task = RunApexTests(self.project_config, task_config, org_config) assert task.code_coverage_level == 90 def test_code_coverage_percentage(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "required_org_code_coverage_percent": "90%", } org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) task = RunApexTests(self.project_config, task_config, org_config) assert task.code_coverage_level == 90 def test_exception_bad_code_coverage(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "required_org_code_coverage_percent": "foo", } with self.assertRaises(TaskOptionsError): RunApexTests(self.project_config, task_config, self.org_config) @responses.activate def test_run_task__code_coverage_managed(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_get_failed_test_classes() self._mock_tests_complete() self._mock_get_test_results() task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "namespace": "TEST", "required_org_code_coverage_percent": "90", } org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) org_config._installed_packages = {"TEST": StrictVersion("1.2.3")} task = RunApexTests(self.project_config, task_config, org_config) task._check_code_coverage = Mock() task() task._check_code_coverage.assert_not_called() def test_check_code_coverage(self): task = RunApexTests(self.project_config, self.task_config, self.org_config) task.code_coverage_level = 90 task.tooling = Mock() task.tooling.query.return_value = { "records": [{"PercentCovered": 90}], "totalSize": 1, } task._check_code_coverage() task.tooling.query.assert_called_once_with( "SELECT PercentCovered FROM ApexOrgWideCoverage" ) def test_check_code_coverage__fail(self): task = RunApexTests(self.project_config, self.task_config, self.org_config) task.code_coverage_level = 90 task.tooling = Mock() task.tooling.query.return_value = { "records": [{"PercentCovered": 89}], "totalSize": 1, } with self.assertRaises(ApexTestException): task._check_code_coverage() def test_is_retriable_failure(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": [ "UNABLE_TO_LOCK_ROW", "unable to obtain exclusive access to this record", ], } task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) self.assertTrue( task._is_retriable_failure( { "Message": "UNABLE_TO_LOCK_ROW", "StackTrace": "test", "Outcome": "Fail", } ) ) self.assertTrue( task._is_retriable_failure( { "Message": "TEST", "StackTrace": "unable to obtain exclusive access to this record", "Outcome": "Fail", } ) ) self.assertFalse( task._is_retriable_failure( { "Message": "DUPLICATES_DETECTED", "StackTrace": "test", "Outcome": "Fail", } ) ) def test_init_options__regexes(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) self.assertIsNotNone( task.options["retry_failures"][0].search("UNABLE_TO_LOCK_ROW: test failed") ) def test_init_options__bad_regexes(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["("], } with self.assertRaises(TaskOptionsError): task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) def test_get_namespace_filter__managed(self): task_config = TaskConfig({"options": {"managed": True, "namespace": "testns"}}) task = RunApexTests(self.project_config, task_config, self.org_config) namespace = task._get_namespace_filter() self.assertEqual("'testns'", namespace) def test_get_namespace_filter__managed_no_namespace(self): task_config = TaskConfig({"options": {"managed": True}}) task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task._get_namespace_filter() def test_get_test_class_query__exclude(self): task_config = TaskConfig( {"options": {"test_name_match": "%_TEST", "test_name_exclude": "EXCL"}} ) task = RunApexTests(self.project_config, task_config, self.org_config) query = task._get_test_class_query() self.assertEqual( "SELECT Id, Name FROM ApexClass WHERE NamespacePrefix = null " "AND (Name LIKE '%_TEST') AND (NOT Name LIKE 'EXCL')", query, ) def test_run_task__no_tests(self): task = RunApexTests(self.project_config, self.task_config, self.org_config) task._get_test_classes = MagicMock(return_value={"totalSize": 0}) task() self.assertIsNone(task.result)
class TestRunApexTests(unittest.TestCase): def setUp(self): self.api_version = 38.0 self.global_config = BaseGlobalConfig( {'project': {'api_version': self.api_version}}) self.task_config = TaskConfig() self.task_config.config['options'] = { 'junit_output': 'results_junit.xml', 'poll_interval': 1, 'test_name_match': '%_TEST', } self.project_config = BaseProjectConfig(self.global_config) self.project_config.config['project'] = {'package': { 'api_version': self.api_version}} keychain = BaseProjectKeychain(self.project_config, '') app_config = ConnectedAppOAuthConfig() keychain.set_connected_app(app_config) self.project_config.set_keychain(keychain) self.org_config = OrgConfig({ 'id': 'foo/1', 'instance_url': 'example.com', 'access_token': 'abc123', }) self.base_tooling_url = 'https://{}/services/data/v{}/tooling/'.format( self.org_config.instance_url, self.api_version) def _mock_apex_class_query(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Name+' + 'FROM+ApexClass+WHERE+NamespacePrefix+%3D+null' + '+AND+%28Name+LIKE+%27%25_TEST%27%29') expected_response = { 'done': True, 'records': [{'Id': 1, 'Name': 'TestClass_TEST'}], 'totalSize': 1, } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_get_test_results(self): url = (self.base_tooling_url + 'query/?q=SELECT+StackTrace%2C+' + 'Message%2C+ApexLogId%2C+AsyncApexJobId%2C+MethodName%2C+' + 'Outcome%2C+ApexClassId%2C+TestTimestamp+FROM+ApexTestResult+' + 'WHERE+AsyncApexJobId+%3D+%27JOB_ID1234567%27') expected_response = { 'done': True, 'records': [{ 'ApexClassId': 1, 'ApexLogId': 1, 'Id': 1, 'Message': 'Test passed', 'MethodName': 'TestMethod', 'Name': 'TestClass_TEST', 'Outcome': 'Pass', 'StackTrace': '1. ParentFunction\n2. ChildFunction', 'Status': 'Completed', }], } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_tests_complete(self): url = (self.base_tooling_url + 'query/?q=SELECT+Id%2C+Status%2C+' + 'ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27' + 'JOB_ID1234567%27') expected_response = { 'done': True, 'records': [{'Status': 'Completed'}], } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_run_tests(self): url = self.base_tooling_url + 'runTestsAsynchronous' expected_response = 'JOB_ID1234567' responses.add(responses.POST, url, json=expected_response) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests( self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 4)
class TestRunApexTests(unittest.TestCase): def setUp(self): self.api_version = 38.0 self.global_config = BaseGlobalConfig( {"project": { "api_version": self.api_version }}) self.task_config = TaskConfig() self.task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", } self.project_config = BaseProjectConfig(self.global_config, config={"noyaml": True}) self.project_config.config["project"] = { "package": { "api_version": self.api_version } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version) def _mock_apex_class_query(self): url = (self.base_tooling_url + "query/?q=SELECT+Id%2C+Name+" + "FROM+ApexClass+WHERE+NamespacePrefix+%3D+null" + "+AND+%28Name+LIKE+%27%25_TEST%27%29") expected_response = { "done": True, "records": [{ "Id": 1, "Name": "TestClass_TEST" }], "totalSize": 1, } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _get_mock_test_query_results(self, outcome, message): return { "done": True, "records": [{ "attributes": { "type": "ApexTestResult", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResult/07M41000009gbT3EAI", }, "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v40.0/tooling/sobjects/ApexClass/01p4100000Fu4Z0AAJ", }, "Name": "EP_TaskDependency_TEST", }, "ApexClassId": 1, "ApexLogId": 1, "TestTimestamp": "2017-07-18T20:36:04.000+0000", "Id": "07M41000009gbT3EAI", "Message": message, "MethodName": "TestMethod", "Outcome": outcome, "QueueItem": { "attributes": { "type": "ApexTestQueueItem", "url": "/services/data/v40.0/tooling/sobjects/ApexTestQueueItem/70941000000q7VsAAI", }, "Status": "Completed", "ExtendedStatus": "(4/4)", }, "RunTime": 1707, "StackTrace": "1. ParentFunction\n2. ChildFunction", "Status": "Completed", "Name": "TestClass_TEST", "ApexTestResults": { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "ApexTestResultLimits", "records": [{ "attributes": { "type": "ApexTestResultLimits", "url": "/services/data/v40.0/tooling/sobjects/ApexTestResultLimits/05n41000002Y7OQAA0", }, "Id": "05n41000002Y7OQAA0", "Callouts": 0, "AsyncCalls": 0, "DmlRows": 5, "Email": 0, "LimitContext": "SYNC", "LimitExceptions": None, "MobilePush": 0, "QueryRows": 20, "Sosl": 0, "Cpu": 471, "Dml": 4, "Soql": 5, }], }, }], } def _get_mock_test_query_url(self, job_id): return ( self.base_tooling_url + "query/?q=%0ASELECT+Id%2CApexClassId%2CTestTimestamp%2C%0A+++++++Message%2CMethodName%2COutcome%2C%0A+++++++RunTime%2CStackTrace%2C%0A+++++++%28SELECT+%0A++++++++++Id%2CCallouts%2CAsyncCalls%2CDmlRows%2CEmail%2C%0A++++++++++LimitContext%2CLimitExceptions%2CMobilePush%2C%0A++++++++++QueryRows%2CSosl%2CCpu%2CDml%2CSoql+%0A++++++++FROM+ApexTestResults%29+%0AFROM+ApexTestResult+%0AWHERE+AsyncApexJobId%3D%27{}%27%0A" .format(job_id)) def _mock_get_test_results(self, outcome="Pass", message="Test Passed", job_id="JOB_ID1234567"): url = self._get_mock_test_query_url(job_id) expected_response = self._get_mock_test_query_results(outcome, message) responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_tests_complete(self, job_id="JOB_ID1234567"): url = (self.base_tooling_url + "query/?q=SELECT+Id%2C+Status%2C+" + "ApexClassId+FROM+ApexTestQueueItem+WHERE+ParentJobId+%3D+%27" + "{}%27".format(job_id)) expected_response = { "done": True, "records": [{ "Status": "Completed" }] } responses.add(responses.GET, url, match_querystring=True, json=expected_response) def _mock_run_tests(self, success=True, body="JOB_ID1234567"): url = self.base_tooling_url + "runTestsAsynchronous" if success: responses.add(responses.POST, url, json=body) else: responses.add(responses.POST, url, status=http.client.SERVICE_UNAVAILABLE) @responses.activate def test_run_task(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results() task = RunApexTests(self.project_config, self.task_config, self.org_config) task() self.assertEqual(len(responses.calls), 4) @responses.activate def test_run_task__server_error(self): self._mock_apex_class_query() self._mock_run_tests(success=False) task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(SalesforceGeneralError): task() @responses.activate def test_run_task__failed(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_tests_complete() self._mock_get_test_results("Fail") task = RunApexTests(self.project_config, self.task_config, self.org_config) with self.assertRaises(ApexTestException): task() @responses.activate def test_run_task__retry_tests(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_run_tests(body="JOBID_9999") self._mock_tests_complete() self._mock_tests_complete(job_id="JOBID_9999") self._mock_get_test_results("Fail", "UNABLE_TO_LOCK_ROW") self._mock_get_test_results(job_id="JOBID_9999") task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) task() self.assertEqual(len(responses.calls), 7) @responses.activate def test_run_task__retry_tests_fails(self): self._mock_apex_class_query() self._mock_run_tests() self._mock_run_tests(body="JOBID_9999") self._mock_tests_complete() self._mock_tests_complete(job_id="JOBID_9999") self._mock_get_test_results("Fail", "UNABLE_TO_LOCK_ROW") self._mock_get_test_results("Fail", "DUPLICATES_DETECTED", job_id="JOBID_9999") task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(ApexTestException): task() def test_is_retriable_failure(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": [ "UNABLE_TO_LOCK_ROW", "unable to obtain exclusive access to this record", ], } task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) self.assertTrue( task._is_retriable_failure({ "Message": "UNABLE_TO_LOCK_ROW", "StackTrace": "test", "Outcome": "Fail", })) self.assertTrue( task._is_retriable_failure({ "Message": "TEST", "StackTrace": "unable to obtain exclusive access to this record", "Outcome": "Fail", })) self.assertFalse( task._is_retriable_failure({ "Message": "DUPLICATES_DETECTED", "StackTrace": "test", "Outcome": "Fail", })) self.assertFalse( task._is_retriable_failure({ "Message": "UNABLE_TO_LOCK_ROW", "StackTrace": "test", "Outcome": "Pass", })) def test_init_options__regexes(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["UNABLE_TO_LOCK_ROW"], } task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) self.assertIsNotNone(task.options["retry_failures"][0].search( "UNABLE_TO_LOCK_ROW: test failed")) def test_init_options__bad_regexes(self): task_config = TaskConfig() task_config.config["options"] = { "junit_output": "results_junit.xml", "poll_interval": 1, "test_name_match": "%_TEST", "retry_failures": ["("], } with self.assertRaises(TaskOptionsError): task = RunApexTests(self.project_config, task_config, self.org_config) task._init_options(task_config.config["options"]) def test_get_namespace_filter__managed(self): task_config = TaskConfig( {"options": { "managed": True, "namespace": "testns" }}) task = RunApexTests(self.project_config, task_config, self.org_config) namespace = task._get_namespace_filter() self.assertEqual("'testns'", namespace) def test_get_namespace_filter__managed_no_namespace(self): task_config = TaskConfig({"options": {"managed": True}}) task = RunApexTests(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task._get_namespace_filter() def test_get_test_class_query__exclude(self): task_config = TaskConfig({ "options": { "test_name_match": "%_TEST", "test_name_exclude": "EXCL" } }) task = RunApexTests(self.project_config, task_config, self.org_config) query = task._get_test_class_query() self.assertEqual( "SELECT Id, Name FROM ApexClass WHERE NamespacePrefix = null " "AND (Name LIKE '%_TEST') AND (NOT Name LIKE 'EXCL')", query, ) def test_run_task__no_tests(self): task = RunApexTests(self.project_config, self.task_config, self.org_config) task._get_test_classes = MagicMock(return_value={"totalSize": 0}) task() self.assertIsNone(task.result)
class TestSFDXBaseTask(MockLoggerMixin, unittest.TestCase): """ Tests for the Base Task type """ def setUp(self): self.global_config = BaseGlobalConfig() self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) self.task_config = TaskConfig() keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self._task_log_handler.reset() self.task_log = self._task_log_handler.messages def test_base_task(self): """ The command is prefixed w/ sfdx """ self.task_config.config["options"] = {"command": "force:org", "extra": "--help"} task = SFDXBaseTask(self.project_config, self.task_config) try: task() except CommandException: pass self.assertEqual("sfdx force:org --help", task.options["command"]) @patch( "cumulusci.tasks.sfdx.SFDXOrgTask._update_credentials", MagicMock(return_value=None), ) @patch("cumulusci.tasks.command.Command._run_task", MagicMock(return_value=None)) def test_keychain_org_creds(self): """ Keychain org creds are passed by env var """ self.task_config.config["options"] = {"command": "force:org --help"} access_token = "00d123" org_config = OrgConfig( { "access_token": access_token, "instance_url": "https://test.salesforce.com", }, "test", ) task = SFDXOrgTask(self.project_config, self.task_config, org_config) try: task() except CommandException: pass self.assertIn("SFDX_INSTANCE_URL", task._get_env()) self.assertIn("SFDX_USERNAME", task._get_env()) self.assertIn(access_token, task._get_env()["SFDX_USERNAME"]) def test_scratch_org_username(self): """ Scratch Org credentials are passed by -u flag """ self.task_config.config["options"] = {"command": "force:org --help"} org_config = ScratchOrgConfig({"username": "******"}, "test") task = SFDXOrgTask(self.project_config, self.task_config, org_config) self.assertIn("-u [email protected]", task.options["command"])
class TestRunBatchApex(unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": { "api_version": self.api_version }}) self.task_config = TaskConfig() self.task_config.config["options"] = { "class_name": "ADDR_Seasonal_BATCH", "poll_interval": 1, } self.project_config = BaseProjectConfig(self.global_config, config={"noyaml": True}) self.project_config.config["project"] = { "package": { "api_version": self.api_version } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version) def _get_query_resp(self): return { "size": 1, "totalSize": 1, "done": True, "queryLocator": None, "entityTypeName": "AsyncApexJob", "records": [{ "attributes": { "type": "AsyncApexJob", "url": "/services/data/v43.0/tooling/sobjects/AsyncApexJob/707L0000014nnPHIAY", }, "Id": "707L0000014nnPHIAY", "ApexClass": { "attributes": { "type": "ApexClass", "url": "/services/data/v43.0/tooling/sobjects/ApexClass/01pL000000109ndIAA", }, "Name": "ADDR_Seasonal_BATCH", }, "Status": "Completed", "ExtendedStatus": None, "TotalJobItems": 1, "JobItemsProcessed": 1, "NumberOfErrors": 0, "CreatedDate": "2018-08-07T16:00:56.000+0000", "CompletedDate": "2018-08-07T16:01:57.000+0000", }], } def _get_url_and_task(self): task = BatchApexWait(self.project_config, self.task_config, self.org_config) url = ( self.base_tooling_url + "query/?q=SELECT+Id%2C+ApexClass.Name%2C+Status%2C+ExtendedStatus%2C+TotalJobItems%2C+JobItemsProcessed%2C+NumberOfErrors%2C+CreatedDate%2C+CompletedDate+FROM+AsyncApexJob+WHERE+JobType%3D%27BatchApex%27+AND+ApexClass.Name%3D%27ADDR_Seasonal_BATCH%27+ORDER+BY+CreatedDate+DESC+LIMIT+1" ) return task, url @responses.activate def test_run_batch_apex_status_fail(self): task, url = self._get_url_and_task() response = self._get_query_resp() response["records"][0]["NumberOfErrors"] = 1 response["records"][0]["ExtendedStatus"] = "Bad Status" responses.add(responses.GET, url, json=response) with self.assertRaises(SalesforceException) as cm: task() err = cm.exception self.assertEqual(err.args[0], "Bad Status") @responses.activate def test_run_batch_apex_status_ok(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() @responses.activate def test_run_batch_apex_calc_delta(self): task, url = self._get_url_and_task() response = self._get_query_resp() responses.add(responses.GET, url, json=response) task() self.assertEqual(task.delta, 61)
class TestAnonymousApexTask(unittest.TestCase): def setUp(self): self.api_version = 42.0 self.global_config = BaseGlobalConfig( {"project": {"api_version": self.api_version}} ) self.tmpdir = tempfile.mkdtemp(dir=".") apex_path = os.path.join(self.tmpdir, "test.apex") with open(apex_path, "w") as f: f.write('System.debug("from file")') self.task_config = TaskConfig() self.task_config.config["options"] = { "path": apex_path, "apex": 'system.debug("Hello World!")', "namespaced": True, } self.project_config = BaseProjectConfig( self.global_config, config={"noyaml": True} ) self.project_config.config = { "project": { "package": {"namespace": "abc", "api_version": self.api_version} } } keychain = BaseProjectKeychain(self.project_config, "") self.project_config.set_keychain(keychain) self.org_config = OrgConfig( { "id": "foo/1", "instance_url": "https://example.com", "access_token": "abc123", }, "test", ) self.base_tooling_url = "{}/services/data/v{}/tooling/".format( self.org_config.instance_url, self.api_version ) def tearDown(self): shutil.rmtree(self.tmpdir) def _get_url_and_task(self): task = AnonymousApexTask(self.project_config, self.task_config, self.org_config) url = self.base_tooling_url + "executeAnonymous" return task, url def test_validate_options(self): task_config = TaskConfig({}) with self.assertRaises(TaskOptionsError): AnonymousApexTask(self.project_config, task_config, self.org_config) def test_run_from_path_outside_repo(self): task_config = TaskConfig({"options": {"path": "/"}}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task() def test_run_path_not_found(self): task_config = TaskConfig({"options": {"path": "bogus"}}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) with self.assertRaises(TaskOptionsError): task() def test_prepare_apex(self): task = AnonymousApexTask(self.project_config, self.task_config, self.org_config) before = "String %%%NAMESPACE%%%str = 'foo';" expected = "String abc__str = 'foo';" self.assertEqual(expected, task._prepare_apex(before)) @responses.activate def test_run_anonymous_apex_success(self): task, url = self._get_url_and_task() resp = {"compiled": True, "success": True} responses.add(responses.GET, url, status=200, json=resp) task() @responses.activate def test_run_string_only(self): task_config = TaskConfig({"options": {"apex": 'System.debug("test");'}}) task = AnonymousApexTask(self.project_config, task_config, self.org_config) url = self.base_tooling_url + "executeAnonymous" responses.add( responses.GET, url, status=200, json={"compiled": True, "success": True} ) task() @responses.activate def test_run_anonymous_apex_status_fail(self): task, url = self._get_url_and_task() responses.add(responses.GET, url, status=418, body="I'm a teapot") with self.assertRaises(SalesforceGeneralError) as cm: task() err = cm.exception self.assertEqual(str(err), "Error Code 418. Response content: I'm a teapot") self.assertTrue(err.url.startswith(url)) self.assertEqual(err.status, 418) self.assertEqual(err.content, "I'm a teapot") @responses.activate def test_run_anonymous_apex_compile_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." resp = { "compiled": False, "compileProblem": problem, "success": False, "line": 1, "column": 13, "exceptionMessage": "", "exceptionStackTrace": "", "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexCompilationException) as cm: task() err = cm.exception self.assertEqual(err.args[0], 1) self.assertEqual(err.args[1], problem) @responses.activate def test_run_anonymous_apex_except(self): task, url = self._get_url_and_task() problem = "Unexpected token '('." trace = "Line 0, Column 99" resp = { "compiled": True, "compileProblem": "", "success": False, "line": 1, "column": 13, "exceptionMessage": problem, "exceptionStackTrace": trace, "logs": "", } responses.add(responses.GET, url, status=200, json=resp) with self.assertRaises(ApexException) as cm: task() err = cm.exception self.assertEqual(err.args[0], problem) self.assertEqual(err.args[1], trace)