예제 #1
0
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]
예제 #2
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"
예제 #3
0
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
예제 #4
0
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")
예제 #5
0
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()
예제 #6
0
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
예제 #7
0
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 == ""
예제 #8
0
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]
예제 #9
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
예제 #10
0
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]
예제 #11
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]
예제 #12
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()
예제 #13
0
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
예제 #14
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)
예제 #15
0
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()
예제 #16
0
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]
예제 #17
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
예제 #18
0
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]
예제 #19
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)
예제 #20
0
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 == ""
예제 #21
0
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]
예제 #22
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 == ""
예제 #23
0
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 == ""
예제 #24
0
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)
예제 #25
0
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)
예제 #26
0
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"
예제 #27
0
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)
예제 #28
0
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)
예제 #29
0
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
예제 #30
0
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