def test_get_default_org(self): keychain = self.keychain_class(self.project_config, self.key) org_config = self.org_config.config.copy() org_config = OrgConfig(org_config, "test", keychain=keychain) org_config.save() keychain.set_default_org("test") org_config.config["default"] = True self.assertEqual(keychain.get_default_org()[1].config, org_config.config)
def test_get_default_org__file_missing_fallback(self): keychain = self.keychain_class(self.project_config, self.key) org_config = OrgConfig(self.org_config.config.copy(), "test", keychain=keychain) org_config.config["default"] = True org_config.save() self.assertEqual(keychain.get_default_org()[1].config, org_config.config)
def test_get_default_org__with_files(self): keychain = self.keychain_class(self.project_config, self.key) org_config = OrgConfig(self.org_config.config.copy(), "test", keychain=keychain) org_config.save() with open(self._default_org_path(), "w") as f: f.write("test") try: self.assertEqual(keychain.get_default_org()[1].config, org_config.config) finally: self._default_org_path().unlink()
def run_flows( *, plan: Plan, skip_steps: list[str], result_class: Union[type[Job], type[PreflightResult]], result_id: int, ): """ This operates with side effects; it changes things in a Salesforce org, and then records the results of those operations on to a `result`. Args: plan (Plan): The Plan instance for the flow you're running. skip_steps (List[str]): The strings in the list should be valid step_num values for steps in this flow. result_class (Union[Type[Job], Type[PreflightResult]]): The type of the instance onto which to record the results of running steps in the flow. Either a PreflightResult or a Job, as appropriate. result_id (int): the PK of the result instance to get. """ result = result_class.objects.get(pk=result_id) scratch_org = None if not result.user: # This means we're in a ScratchOrg. scratch_org = ScratchOrg.objects.get(org_id=result.org_id) repo_url = plan.version.product.repo_url commit_ish = plan.commit_ish or plan.version.commit_ish with contextlib.ExitStack() as stack: stack.enter_context(finalize_result(result)) if result.user: stack.enter_context(report_errors_to(result.user)) if scratch_org: stack.enter_context(delete_org_on_error(scratch_org)) # Let's clone the repo locally: repo_user, repo_name = extract_user_and_repo(repo_url) repo_root = stack.enter_context( local_github_checkout(repo_user, repo_name, commit_ish) ) # Get cwd into Python path, so that the tasks below can import # from the checked-out repo: stack.enter_context(prepend_python_path(os.path.abspath(repo_root))) # There's a lot of setup to make configs and keychains, link # them properly, and then eventually pass them into a flow, # which we then run: ctx = MetaDeployCCI(repo_root=repo_root, plan=plan) current_org = "current_org" if settings.METADEPLOY_FAST_FORWARD: # pragma: no cover org_config = OrgConfig({}, name=current_org, keychain=ctx.keychain) elif scratch_org: org_config = scratch_org.get_refreshed_org_config( org_name=current_org, keychain=ctx.keychain ) else: token, token_secret = result.user.token org_config = OrgConfig( { "access_token": token, "instance_url": result.user.instance_url, "refresh_token": token_secret, "username": result.user.sf_username, # 'id' is used by CumulusCI to pick the right 'aud' for JWT auth "id": result.user.oauth_id, }, current_org, keychain=ctx.keychain, ) org_config.save() # Set up the connected_app: connected_app = ServiceConfig( { "client_secret": settings.SFDX_CLIENT_SECRET, "callback_url": settings.SFDX_CLIENT_CALLBACK_URL, "client_id": settings.SFDX_CLIENT_ID, # Note: login_url is not used when refreshing the existing token, # so it doesn't matter whether it's login vs test "login_url": "https://login.salesforce.com", } ) ctx.keychain.set_service("connected_app", "metadeploy", connected_app) ctx.keychain._default_services["connected_app"] = "metadeploy" steps = [ step.to_spec( project_config=ctx.project_config, skip=step.step_num in skip_steps ) for step in plan.steps.all() ] org = ctx.keychain.get_org(current_org) if not settings.METADEPLOY_FAST_FORWARD: result.run(ctx, plan, steps, org)
class TestEncryptedFileProjectKeychain(ProjectKeychainTestMixin): keychain_class = EncryptedFileProjectKeychain def setUp(self): self.universal_config = UniversalConfig() self.project_config = BaseProjectConfig( self.universal_config, config={"noyaml": True} ) self.project_config.config["services"] = { "connected_app": {"attributes": {"test": {"required": True}}}, "github": {"attributes": {"git": {"required": True}, "password": {}}}, "not_configured": {"attributes": {"foo": {"required": True}}}, } self.project_config.project__name = "TestProject" self.project_name = "TestProject" self.org_config = OrgConfig({"foo": "bar"}, "test") self.scratch_org_config = ScratchOrgConfig( {"foo": "bar", "scratch": True}, "test_scratch" ) self.services = { "connected_app": ServiceConfig({"test": "value"}), "github": ServiceConfig({"git": "hub"}), } self.key = "0123456789123456" self._mk_temp_home() self._home_patch = mock.patch( "pathlib.Path.home", return_value=Path(self.tempdir_home) ) self._home_patch.__enter__() self._mk_temp_project() os.chdir(self.tempdir_project) def tearDown(self): self._home_patch.__exit__(None, None, None) def _mk_temp_home(self): self.tempdir_home = tempfile.mkdtemp() global_config_dir = os.path.join(self.tempdir_home, ".cumulusci") os.makedirs(global_config_dir) def _mk_temp_project(self): self.tempdir_project = tempfile.mkdtemp() git_dir = os.path.join(self.tempdir_project, ".git") os.makedirs(git_dir) self._create_git_config() def _create_git_config(self): filename = os.path.join(self.tempdir_project, ".git", "config") content = ( '[remote "origin"]\n' + f" url = [email protected]:TestOwner/{self.project_name}" ) self._write_file(filename, content) def _write_file(self, filename, content): with open(filename, "w") as f: f.write(content) def test_set_service_github_project(self): self.test_set_service_github(True) def test_set_and_get_org_global(self): self.test_set_and_get_org(True) def test_set_and_get_org__universal_config(self): keychain = self.keychain_class(self.universal_config, self.key) keychain.set_org(self.org_config, False) self.assertEqual(list(keychain.orgs.keys()), []) def test_load_files__empty(self): dummy_keychain = BaseEncryptedProjectKeychain(self.project_config, self.key) os.makedirs(os.path.join(self.tempdir_home, ".cumulusci", self.project_name)) self._write_file( os.path.join(self.tempdir_home, "test.org"), dummy_keychain._encrypt_config(BaseConfig({"foo": "bar"})).decode("utf-8"), ) keychain = self.keychain_class(self.project_config, self.key) del keychain.config["orgs"] with mock.patch.object( self.keychain_class, "global_config_dir", Path(self.tempdir_home) ): keychain._load_orgs() self.assertIn("foo", keychain.get_org("test").config) self.assertEqual(keychain.get_org("test").keychain, keychain) def test_load_file(self): self._write_file(os.path.join(self.tempdir_home, "config"), "foo") keychain = self.keychain_class(self.project_config, self.key) keychain._load_file(self.tempdir_home, "config", "from_file") self.assertEqual("foo", keychain.config["from_file"]) def test_load_file__universal_config(self): self._write_file(os.path.join(self.tempdir_home, "config"), "foo") keychain = self.keychain_class(self.project_config, self.key) keychain._load_file(self.tempdir_home, "config", "from_file") self.assertEqual("foo", keychain.config["from_file"]) @mock.patch("cumulusci.core.utils.cleanup_org_cache_dirs") def test_remove_org(self, cleanup_org_cache_dirs): keychain = self.keychain_class(self.project_config, self.key) keychain.set_org(self.org_config) keychain.remove_org("test") self.assertNotIn("test", keychain.orgs) assert cleanup_org_cache_dirs.called_once_with(keychain, self.project_config) def test_remove_org__not_found(self): keychain = self.keychain_class(self.project_config, self.key) keychain.orgs["test"] = mock.Mock() with self.assertRaises(OrgNotFound): keychain.remove_org("test") def test_remove_org__global__not_found(self): keychain = self.keychain_class(self.project_config, self.key) keychain.orgs["test"] = mock.Mock() with self.assertRaises(OrgNotFound): keychain.remove_org("test", global_org=True) def test_set_and_get_org_local_should_not_shadow_global(self): keychain = self.keychain_class(self.project_config, self.key) self.org_config.global_org = True keychain.set_org(self.org_config, global_org=True) assert ["test"] == list(keychain.orgs.keys()) assert isinstance(keychain.orgs["test"], GlobalOrg), keychain.orgs["test"] assert self.org_config.config == keychain.get_org("test").config assert Path(self.tempdir_home, ".cumulusci", "test.org").exists() # check that it saves to the right place with mock.patch( "cumulusci.core.keychain.encrypted_file_project_keychain.open" ) as o: self.org_config.save() opened_filename = o.mock_calls[0][1][0] assert ".cumulusci/test.org" in opened_filename.replace( os.sep, "/" ), opened_filename # check that it can be loaded in a fresh keychain new_keychain = self.keychain_class(self.project_config, self.key) org_config = new_keychain.get_org("test") assert org_config.global_org def test_cache_dir(self): keychain = self.keychain_class(self.project_config, self.key) assert keychain.cache_dir.name == ".cci" def test_get_default_org__with_files(self): keychain = self.keychain_class(self.project_config, self.key) org_config = OrgConfig(self.org_config.config.copy(), "test", keychain=keychain) org_config.save() with open(self._default_org_path(), "w") as f: f.write("test") try: self.assertEqual(keychain.get_default_org()[1].config, org_config.config) finally: self._default_org_path().unlink() def test_get_default_org__with_files__missing_org(self): keychain = self.keychain_class(self.project_config, self.key) with open(self._default_org_path(), "w") as f: f.write("should_not_exist") assert self._default_org_path().exists() assert keychain.get_default_org() == (None, None) assert not self._default_org_path().exists() @mock.patch("sarge.Command") def test_set_default_org__with_files(self, Command): keychain = self.keychain_class(self.project_config, self.key) org_config = OrgConfig(self.org_config.config.copy(), "test") keychain.set_org(org_config) keychain.set_default_org("test") with open(self._default_org_path()) as f: assert f.read() == "test" self._default_org_path().unlink() @mock.patch("sarge.Command") def test_unset_default_org__with_files(self, Command): keychain = self.keychain_class(self.project_config, self.key) org_config = self.org_config.config.copy() org_config = OrgConfig(org_config, "test") keychain.set_org(org_config) keychain.set_default_org("test") keychain.unset_default_org() self.assertEqual(keychain.get_default_org()[1], None) assert not self._default_org_path().exists() def _default_org_path(self): return Path(self.tempdir_home) / ".cumulusci/TestProject/DEFAULT_ORG.txt" # old way of finding defaults used contents of the files themselves # we should preserve backwards compatibiliity for a few months def test_get_default_org__file_missing_fallback(self): keychain = self.keychain_class(self.project_config, self.key) org_config = OrgConfig(self.org_config.config.copy(), "test", keychain=keychain) org_config.config["default"] = True org_config.save() self.assertEqual(keychain.get_default_org()[1].config, org_config.config) def test_get_default_org__outside_project(self): keychain = self.keychain_class(self.universal_config, self.key) assert keychain.get_default_org() == (None, None)
def run_flows(*, user, plan, skip_steps, organization_url, result_class, result_id): """ This operates with side effects; it changes things in a Salesforce org, and then records the results of those operations on to a `result`. Args: user (User): The User requesting this flow be run. plan (Plan): The Plan instance for the flow you're running. skip_steps (List[str]): The strings in the list should be valid step_num values for steps in this flow. organization_url (str): The URL of the organization, required by the OrgConfig. result_class (Union[Type[Job], Type[PreflightResult]]): The type of the instance onto which to record the results of running steps in the flow. Either a PreflightResult or a Job, as appropriate. result_id (int): the PK of the result instance to get. """ result = result_class.objects.get(pk=result_id) token, token_secret = user.token repo_url = plan.version.product.repo_url commit_ish = plan.commit_ish or plan.version.commit_ish with contextlib.ExitStack() as stack: stack.enter_context(finalize_result(result)) stack.enter_context(report_errors_to(user)) tmpdirname = stack.enter_context(temporary_dir()) # Get cwd into Python path, so that the tasks below can import # from the checked-out repo: stack.enter_context(prepend_python_path(os.path.abspath(tmpdirname))) # Let's clone the repo locally: user, repo_name = extract_user_and_repo(repo_url) gh = get_github_api_for_repo(None, user, repo_name) repo = gh.repository(user, repo_name) # Make sure we have the actual owner/repo name if we were redirected user = repo.owner.login repo_name = repo.name zip_file_name = "archive.zip" repo.archive("zipball", path=zip_file_name, ref=commit_ish) zip_file = zipfile.ZipFile(zip_file_name) if not zip_file_is_safe(zip_file): # This is very unlikely, as we get the zipfile from GitHub, # but must be considered: url = f"https://github.com/{user}/{repo_name}#{commit_ish}" logger.error(f"Malformed or malicious zip file from {url}.") return zip_file.extractall() # We know that the zipball contains a root directory named # something like this by GitHub's convention. If that ever # breaks, this will break: zipball_root = glob(f"{user}-{repo_name}-*")[0] # It's not unlikely that the zipball root contains a directory # with the same name, so we pre-emptively rename it to probably # avoid collisions: shutil.move(zipball_root, "zipball_root") for path in itertools.chain(glob("zipball_root/*"), glob("zipball_root/.*")): shutil.move(path, ".") shutil.rmtree("zipball_root") # There's a lot of setup to make configs and keychains, link # them properly, and then eventually pass them into a flow, # which we then run: ctx = MetaDeployCCI(repo_root=tmpdirname, plan=plan) current_org = "current_org" org_config = OrgConfig( { "access_token": token, "instance_url": organization_url, "refresh_token": token_secret, }, current_org, keychain=ctx.keychain, ) org_config.save() # Set up the connected_app: connected_app = ServiceConfig({ "client_secret": settings.CONNECTED_APP_CLIENT_SECRET, "callback_url": settings.CONNECTED_APP_CALLBACK_URL, "client_id": settings.CONNECTED_APP_CLIENT_ID, }) ctx.keychain.set_service("connected_app", connected_app, True) steps = [ step.to_spec(project_config=ctx.project_config, skip=step.step_num in skip_steps) for step in plan.steps.all() ] org = ctx.keychain.get_org(current_org) result.run(ctx, plan, steps, org)