def test_select_auth_no_match(auth_config, tmpdir): no_match = "No matching credential found" # no credentials at all with pytest.raises(RuntimeError) as err: selected = auth.select_matching_auth([], "example.com", "nosuchuser") assert "No matching credential found for hostname 'example.com'" in err.value.args[ 0] # no match for requested hostname with_host = """auth = [{ username = "******", password = "******", hostname = "example.com" }]""" with temp_config(tmpdir, with_host) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(RuntimeError) as err: creds = auth.load_auth() selected = auth.select_matching_auth(creds, "example.net") assert f"{no_match} for hostname 'example.net'" in err.value.args[0] # no match for requested username with temp_config(tmpdir, auth_config) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(RuntimeError) as err: creds = auth.load_auth() selected = auth.select_matching_auth(creds, "example.com", "nosuchuser") assert f"{no_match} for hostname 'example.com' with username 'nosuchuser'" in err.value.args[ 0]
def test_load_auth_options(auth_config, tmpdir): # SSL should be used by default # The default mechanism should be SCRAM_SHA_512 with temp_config(tmpdir, auth_config) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl=True) from adc.auth import SASLMethod assert auth_mock.called_with(mechanism=SASLMethod.SCRAM_SHA_512) # But it should be possible to disable SSL use_plaintext = """auth = [{ username = "******", password = "******", protocol = "SASL_PLAINTEXT" }]""" with temp_config(tmpdir, use_plaintext) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl=False) # An SSL CA data path should be honored with_ca_data = """auth = [{ username = "******", password = "******", ssl_ca_location = "/foo/bar/baz" }]""" with temp_config(tmpdir, with_ca_data) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl_ca_location="/foo/bar/baz") # Alternate mechanisms should be honored plain_mechanism = """auth = [{ username = "******", password = "******", mechanism = "PLAIN" }]""" with temp_config(tmpdir, plain_mechanism) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(mechanism=SASLMethod.PLAIN) # Associated hostnames should be included with_host = """auth = [{ username = "******", password = "******", hostname = "example.com" }]""" with temp_config(tmpdir, with_host) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir): creds = auth.load_auth() assert len(creds) == 1 assert creds[0].hostname == "example.com"
def test_setup_auth(script_runner, tmpdir): with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): credentials_file = tmpdir / "credentials.csv" username = "******" password = "******" with open(credentials_file, "w") as f: f.write("username,password\n") f.write(username + "," + password + "\n") # check on new configuration file is written using credential file ret1 = script_runner.run("hop", "configure", "setup", "--import", str(credentials_file)) assert ret1.success assert "hop : INFO : Generated configuration at:" in ret1.stderr configuration_file = configure.get_config_path() cf = open(configuration_file, "r") config_file_text = cf.read() assert username in config_file_text assert password in config_file_text os.remove(credentials_file) # hop configure setup (need --force) warning_message = \ "hop : WARNING : Configuration already exists, overwrite file with --force" ret2 = script_runner.run("hop", "configure", "setup") assert warning_message in ret2.stderr
def test_stream_open(auth_config, tmpdir): stream = io.Stream(auth=False) # verify only read/writes are allowed with pytest.raises(ValueError) as err: stream.open("kafka://localhost:9092/topic1", "q") assert "mode must be either 'w' or 'r'" in err.value.args # verify that URLs with no scheme are rejected with pytest.raises(ValueError) as err: stream.open("bad://example.com/topic", "r") assert "invalid kafka URL: must start with 'kafka://'" in err.value.args # verify that URLs with no topic are rejected with pytest.raises(ValueError) as err: stream.open("kafka://example.com/", "r") assert "no topic(s) specified in kafka URL" in err.value.args # verify that complete URLs are accepted with temp_config(tmpdir, auth_config) as config_dir, temp_environ(XDG_CONFIG_HOME=config_dir), \ patch("adc.consumer.Consumer.subscribe", MagicMock()) as subscribe: stream = io.Stream() # opening a valid URL for reading should succeed consumer = stream.open("kafka://example.com/topic", "r") # an appropriate consumer group name should be derived from the username in the auth assert consumer._consumer.conf.group_id.startswith( stream.auth.username) # the target topic should be subscribed to subscribe.assert_called_once_with("topic") # opening a valid URL for writing should succeed producer = stream.open("kafka://example.com/topic", "w") producer.write("data")
def test_load_auth_malformed(tmpdir): missing_username = """ auth = [{extra="stuff", password="******"}] """ with temp_config(tmpdir, missing_username) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(RuntimeError): auth.load_auth() missing_password = """ auth = [{username="******", extra="stuff"}] """ with temp_config(tmpdir, missing_password) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(RuntimeError): auth.load_auth()
def test_prune_outdated_auth(tmpdir): config_data = """ foo = "bar" auth = [{ username = "******", password = "******", hostname = "example.com" }] baz = "quux" """ with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): config_path = configure.get_config_path() os.makedirs(os.path.dirname(config_path), exist_ok=True) with open(config_path, "w") as f: f.write(config_data) auth.prune_outdated_auth() # the auth data should be gone new_config_data = check_no_auth_data(config_path) # all other data should remain untouched assert "foo" in new_config_data assert "baz" in new_config_data # the same should work for files in non-default locations alt_config_path = f"{os.path.dirname(config_path)}/other.toml" with open(alt_config_path, "w") as f: f.write(config_data) auth.prune_outdated_auth(alt_config_path) # the auth data should be gone new_config_data = check_no_auth_data(alt_config_path) # all other data should remain untouched assert "foo" in new_config_data assert "baz" in new_config_data
def test_list_credentials(script_runner, auth_config, tmpdir): with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop", "auth", "list") assert ret.success assert "username" in ret.stdout assert ret.stderr == ""
def test_delete_credential_no_match_no_data(tmpdir): invalid_username = "******" with temp_environ(HOME=str(tmpdir)), \ pytest.raises(RuntimeError) as err: auth.delete_credential(invalid_username) assert "No matching credential found" in err.value.args[0] assert invalid_username in err.value.args[0]
def test_get_config_path(tmpdir): with temp_environ(HOME=str(tmpdir)): if "XDG_CONFIG_HOME" in os.environ: # this change will revert at the end of the with block del os.environ["XDG_CONFIG_HOME"] # with HOME set but not XDG_CONFIG_HOME the config location should resolve to inside # ${HOME}/.config expected_path = os.path.join(tmpdir, ".config", "hop", "config.toml") config_loc = configure.get_config_path() assert config_loc == expected_path with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): # with XDG_CONFIG_HOME set, no .config path component should be assumed expected_path = os.path.join(tmpdir, "hop", "config.toml") config_loc = configure.get_config_path() assert config_loc == expected_path
def test_delete_credential_ambiguous_creds_without_hosts(tmpdir): creds = [auth.Auth("user1", "pass1"), auth.Auth("user1", "pass2")] with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=creds)), \ pytest.raises(RuntimeError) as err: auth.delete_credential("user1") assert "Ambiguous credentials found" in err.value.args[0] assert creds[0].username in err.value.args[0]
def test_delete_credential_no_match(tmpdir): invalid_username = "******" with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=delete_input_creds.copy())), \ pytest.raises(RuntimeError) as err: auth.delete_credential(invalid_username) assert "No matching credential found" in err.value.args[0] assert invalid_username in err.value.args[0] invalid_host = "example.com" with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=delete_input_creds.copy())), \ pytest.raises(RuntimeError) as err: auth.delete_credential(f"{invalid_username}@{invalid_host}") assert "No matching credential found" in err.value.args[0] assert invalid_username in err.value.args[0] assert f" with hostname '{invalid_host}'" in err.value.args[0]
def test_load_auth_bad_perms(auth_config, tmpdir): for bad_perm in [ stat.S_IRGRP, stat.S_IWGRP, stat.S_IXGRP, stat.S_IROTH, stat.S_IWOTH, stat.S_IXOTH ]: with temp_config(tmpdir, auth_config, bad_perm) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(RuntimeError): auth.load_auth()
def test_config_advice(script_runner, auth_config, tmpdir): advice_tag = "No valid credential data found" # nonexistent config file with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): ret = script_runner.run("hop") assert advice_tag in ret.stdout # wrong credential file permissions import stat with temp_config(tmpdir, "", stat.S_IROTH) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop") assert advice_tag in ret.stdout assert "unsafe permissions" in ret.stderr # syntactically invalid TOML in credential file garbage = "JVfwteouh '652b" with temp_config( tmpdir, garbage) as config_dir, temp_environ(XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop") assert advice_tag in ret.stdout assert "not configured correctly" in ret.stderr # syntactically valid TOML without an [auth] section toml_no_auth = """title = "TOML Example" [owner] name = "Tom Preston-Werner" dob = 1979-05-27T07:32:00-08:00 """ with temp_config(tmpdir, toml_no_auth) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop") assert advice_tag in ret.stdout assert "configuration file has no auth section" in ret.stderr # syntactically valid TOML an incomplete [auth] section toml_bad_auth = """[auth] foo = "bar" """ with temp_config(tmpdir, toml_bad_auth) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop") assert advice_tag in ret.stdout assert "missing auth property" in ret.stderr
def test_load_auth_options(auth_config, tmpdir): # SSL should be used by default # The default mechanism should be SCRAM_SHA_512 with temp_config(tmpdir, auth_config) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl=True) from adc.auth import SASLMethod assert auth_mock.called_with(mechanism=SASLMethod.SCRAM_SHA_512) # But it should be possible to disable SSL use_plaintext = """ [auth] username = "******" password = "******" protocol = "SASL_PLAINTEXT" """ with temp_config(tmpdir, use_plaintext) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl=False) # An SSL CA data path should be honored with_ca_data = """ [auth] username = "******" password = "******" ssl_ca_location = "/foo/bar/baz" """ with temp_config(tmpdir, with_ca_data) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(ssl_ca_location="/foo/bar/baz") # Alternate mechanisms should be honored plain_mechanism = """ [auth] username = "******" password = "******" mechanism = "PLAIN" """ with temp_config(tmpdir, plain_mechanism) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), patch("hop.auth.Auth") as auth_mock: auth.load_auth() assert auth_mock.called_with(mechanism=SASLMethod.PLAIN)
def test_load_auth_malformed(tmpdir): missing_username = """ [auth] password = "******" extra = "stuff" """ with temp_config(tmpdir, missing_username) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(KeyError): auth.load_auth() missing_password = """ [auth] username = "******" extra = "stuff" """ with temp_config(tmpdir, missing_password) as config_dir, \ temp_environ(XDG_CONFIG_HOME=config_dir), pytest.raises(KeyError): auth.load_auth()
def test_delete_credential_ambiguous(tmpdir): with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=delete_input_creds.copy())), \ pytest.raises(RuntimeError) as err: auth.delete_credential("user3") assert "Ambiguous credentials found" in err.value.args[0] assert delete_input_creds[2].username in err.value.args[0] assert delete_input_creds[2].hostname in err.value.args[0] assert delete_input_creds[3].hostname in err.value.args[0]
def test_add_credential(script_runner, auth_config, tmpdir): with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): csv_file = str(tmpdir) + "/new_cred.csv" with open(csv_file, "w") as f: f.write("username,password\nnew_user,new_pass") ret = script_runner.run("hop", "auth", "add", csv_file) assert ret.success assert "Wrote configuration to" in ret.stderr
def test_delete_credential_ambiguous_with_host(tmpdir): creds = [delete_input_creds[2], delete_input_creds[2]] with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=creds)), \ pytest.raises(RuntimeError) as err: auth.delete_credential("*****@*****.**") assert "Ambiguous credentials found" in err.value.args[0] assert creds[0].username in err.value.args[0] assert creds[0].hostname in err.value.args[0]
def test_add_credential_to_empty(tmpdir): new_cred = auth.Auth("user", "pass") with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.read_new_credential", MagicMock(return_value=new_cred)): args = MagicMock() args.cred_file = None args.force = False auth.add_credential(args) check_credential_file(configure.get_config_path("auth"), new_cred)
def test_cli_version(script_runner, auth_config, tmpdir): with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret = script_runner.run("hop", "version", "--help") assert ret.success assert ret.stderr == "" ret = script_runner.run("hop", "version") assert ret.success assert f"hop-client=={__version__}\n" in ret.stdout assert ret.stderr == ""
def test_prune_outdated_malformed(tmpdir): with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): config_path = configure.get_config_path() os.makedirs(os.path.dirname(config_path), exist_ok=True) with open(config_path, "w") as f: f.write("not valid TOML IGIUF T J2(YHFOh q3pi8hoU *AHou7w3ht") # a RuntimeError should be raised when the file is unparseable garbage with pytest.raises(RuntimeError) as err: auth.prune_outdated_auth() assert f"configuration file {config_path} is malformed" in err.value.args[ 0]
def test_cli_auth(script_runner, auth_config, tmpdir): with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret1 = script_runner.run("hop", "auth", "--help") assert ret1.success assert ret1.stderr == "" ret = script_runner.run("hop", "auth", "locate") assert ret.success assert config_dir in ret.stdout assert ret.stderr == ""
def test_cli_hop_module(script_runner, auth_config, tmpdir): ret = script_runner.run("python", "-m", "hop", "--help") assert ret.success with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): ret = script_runner.run("python", "-m", "hop", "--version") assert ret.success assert f"hop version {__version__}\n" in ret.stdout assert ret.stderr == ""
def test_add_credential_to_nonempty(auth_config, tmpdir): old_cred = auth.Auth("username", "password") new_cred = auth.Auth("other_user", "other_pass") with temp_config(tmpdir, auth_config) as config_dir, temp_environ(HOME=config_dir), \ patch("hop.auth.read_new_credential", MagicMock(return_value=new_cred)): args = MagicMock() args.cred_file = None args.force = False auth.add_credential(args) check_credential_file(configure.get_config_path("auth"), old_cred) check_credential_file(configure.get_config_path("auth"), new_cred)
def test_prune_outdated_empty(tmpdir): with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): # when the config file does not exist, this function should successfully do nothing auth.prune_outdated_auth() config_path = configure.get_config_path() os.makedirs(os.path.dirname(config_path), exist_ok=True) with open(config_path, "w"): pass # should also work when the file exists but is empty auth.prune_outdated_auth() check_no_auth_data(config_path)
def test_stream_auth(auth_config, tmpdir): # turning off authentication should give None for the auth property s1 = io.Stream(auth=False) assert s1.auth is None # turning on authentication should give an auth object with the data read from the default file with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): s2 = io.Stream(auth=True) a2 = s2.auth assert a2._config["sasl.username"] == "username" assert a2._config["sasl.password"] == "password" assert a2.username == "username" # turning on authentication should fail when the default file does not exist with temp_environ( XDG_CONFIG_HOME=str(tmpdir)), pytest.raises(FileNotFoundError): s3 = io.Stream(auth=True) a3 = s3.auth # anything other than a bool passed to the Stream constructor should get handed back unchanged s4 = io.Stream(auth="blarg") assert s4.auth == "blarg"
def test_add_credential_to_nonempty_hostname_no_conflict(tmpdir): # unfortunately, we must permit duplicate usernames if one has a hostname and the other does not old_cred = auth.Auth("username", "password") new_cred = auth.Auth("username", "other_pass", "example.com") with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.load_auth", MagicMock(return_value=[old_cred])), \ patch("hop.auth.read_new_credential", MagicMock(return_value=new_cred)): args = MagicMock() args.cred_file = None args.force = False auth.add_credential(args) check_credential_file(configure.get_config_path("auth"), old_cred) check_credential_file(configure.get_config_path("auth"), new_cred)
def test_add_credential_conflict_no_host(tmpdir, caplog): old_cred = auth.Auth("username", "password") new_cred = auth.Auth("username", "other_pass") with temp_environ(HOME=str(tmpdir)), \ patch("hop.auth.read_new_credential", MagicMock(return_value=new_cred)): auth.write_auth_data(configure.get_config_path("auth"), [old_cred]) args = MagicMock() args.cred_file = None args.force = False auth.add_credential(args) # without the force option, the old credential should not be overwritten check_credential_file(configure.get_config_path("auth"), old_cred) assert "Credential already exists; overwrite with --force" in caplog.text args.force = True auth.add_credential(args) # with the force option, the old credential should be overwritten check_credential_file(configure.get_config_path("auth"), new_cred)
def test_add_credential_overwrite(script_runner, auth_config, tmpdir): with temp_config(tmpdir, auth_config) as config_dir, temp_environ( XDG_CONFIG_HOME=config_dir): csv_file = str(tmpdir) + "/new_cred.csv" with open(csv_file, "w") as f: f.write("username,password\nnew_user,new_pass") ret = script_runner.run("hop", "auth", "add", csv_file) assert ret.success assert "Wrote configuration to" in ret.stderr with open(csv_file, "w") as f: f.write("username,password\nnew_user,other_pass") # try to overwrite the credential, without forcing ret = script_runner.run("hop", "auth", "add", csv_file) assert ret.success assert "Credential already exists; overwrite with --force" in ret.stderr # try again, with force ret = script_runner.run("hop", "auth", "add", "--force", csv_file) assert ret.success assert "Wrote configuration to" in ret.stderr
def test_auth_location_fallback(tmpdir): valid_auth = "auth = [{username=\"user\",password=\"pass\"}]" other_auth = "auth = [{username=\"other-user\",password=\"other-pass\"}]" config_dir = f"{tmpdir}/hop" os.makedirs(config_dir, exist_ok=True) def write_file(name: str, data: str): file_path = f"{config_dir}/{name}" with open(file_path, 'w') as file: os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) file.write(data) with temp_environ(XDG_CONFIG_HOME=str(tmpdir)): # If both auth.toml and config.toml contain auth data, only auth.toml should be read write_file("auth.toml", valid_auth) write_file("config.toml", other_auth) creds = auth.load_auth() assert len(creds) == 1 assert creds[0].username == "user" # If auth.toml does not exist and config.toml contains valid auth data, it should be read os.remove(f"{config_dir}/auth.toml") creds = auth.load_auth() assert len(creds) == 1 assert creds[0].username == "other-user" # If auth.toml does not exist and config.toml exists but contains no valid auth data, the # resulting error should be about auth.toml write_file("config.toml", "") with pytest.raises(FileNotFoundError) as err: creds = auth.load_auth() assert "auth.toml" in err.value.filename # If neither auth.toml nor config.toml exixts, the error should be about auth.toml os.remove(f"{config_dir}/config.toml") with pytest.raises(FileNotFoundError) as err: creds = auth.load_auth() assert "auth.toml" in err.value.filename