Files
2026-07-13 13:17:40 +08:00

290 lines
11 KiB
Python

"""Unit tests for Databricks credential providers."""
import os
from unittest import mock
import pytest
from ray.data._internal.datasource.databricks_credentials import (
DatabricksCredentialProvider,
DatabricksTableCredentialConfig,
EnvironmentCredentialProvider,
StaticCredentialProvider,
UnityCatalogCredentialConfig,
resolve_credential_provider,
)
SAMPLE_TOKEN = "dapi_test_token_abc123"
SAMPLE_HOST = "https://my-workspace.cloud.databricks.com"
SAMPLE_URL = "https://uc-workspace.databricks.com"
ALT_TOKEN = "dapi_alt_token_xyz789"
ALT_HOST = "https://alt-workspace.databricks.com"
class TestDatabricksCredentialProvider:
"""Tests for the abstract DatabricksCredentialProvider base class."""
def test_cannot_instantiate_abstract_class(self):
"""Verify DatabricksCredentialProvider cannot be instantiated directly."""
with pytest.raises(TypeError, match="Can't instantiate abstract class"):
DatabricksCredentialProvider()
def test_abstract_methods_defined(self):
"""Verify all abstract methods are defined."""
abstract_methods = DatabricksCredentialProvider.__abstractmethods__
assert "get_token" in abstract_methods
assert "get_host" in abstract_methods
assert "invalidate" in abstract_methods
class TestStaticCredentialProvider:
"""Tests for StaticCredentialProvider."""
def test_init_with_valid_token_and_host(self):
"""Test successful initialization with token and host."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
assert provider.get_token() == SAMPLE_TOKEN
assert provider.get_host() == SAMPLE_HOST
@pytest.mark.parametrize(
"token,host,expected_error",
[
("", SAMPLE_HOST, "Token cannot be empty"),
(None, SAMPLE_HOST, "Token cannot be empty"),
(SAMPLE_TOKEN, "", "Host cannot be empty"),
(SAMPLE_TOKEN, None, "Host cannot be empty"),
],
)
def test_init_with_invalid_inputs_raises_error(self, token, host, expected_error):
"""Test that invalid token or host raises ValueError."""
with pytest.raises(ValueError, match=expected_error):
StaticCredentialProvider(token=token, host=host)
def test_invalidate_is_noop(self):
"""Test that invalidate doesn't affect the static token."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
provider.invalidate()
assert provider.get_token() == SAMPLE_TOKEN
assert provider.get_host() == SAMPLE_HOST
def test_get_token_returns_same_value(self):
"""Test that get_token always returns the same value."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
assert provider.get_token() == SAMPLE_TOKEN
assert provider.get_token() == SAMPLE_TOKEN
class TestEnvironmentCredentialProvider:
"""Tests for EnvironmentCredentialProvider."""
def test_get_token_from_env(self):
"""Test get_token reads from environment variable."""
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": SAMPLE_TOKEN, "DATABRICKS_HOST": SAMPLE_HOST},
):
provider = EnvironmentCredentialProvider()
assert provider.get_token() == SAMPLE_TOKEN
def test_get_host_from_env(self):
"""Test get_host reads from environment variable."""
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": SAMPLE_TOKEN, "DATABRICKS_HOST": SAMPLE_HOST},
):
provider = EnvironmentCredentialProvider()
assert provider.get_host() == SAMPLE_HOST
@pytest.mark.parametrize(
"env_vars,expected_error",
[
({"DATABRICKS_HOST": SAMPLE_HOST}, "DATABRICKS_TOKEN.*not set"),
(
{"DATABRICKS_TOKEN": SAMPLE_TOKEN},
"set environment variable.*DATABRICKS_HOST",
),
],
)
def test_init_raises_when_env_var_not_set(self, env_vars, expected_error):
"""Test __init__ raises ValueError when required env var is not set."""
with mock.patch.dict(os.environ, env_vars, clear=True):
with pytest.raises(ValueError, match=expected_error):
EnvironmentCredentialProvider()
def test_host_detected_from_databricks_runtime(self):
"""Test host is detected from Databricks runtime when env var not set."""
detected_host = "detected-host.databricks.com"
with (
mock.patch.dict(os.environ, {"DATABRICKS_TOKEN": SAMPLE_TOKEN}, clear=True),
mock.patch.object(
EnvironmentCredentialProvider,
"_detect_databricks_host",
return_value=detected_host,
),
):
provider = EnvironmentCredentialProvider()
assert provider.get_host() == detected_host
def test_custom_env_var_names(self):
"""Test using custom environment variable names."""
with mock.patch.dict(
os.environ, {"MY_TOKEN": SAMPLE_TOKEN, "MY_HOST": SAMPLE_HOST}
):
provider = EnvironmentCredentialProvider(
token_env_var="MY_TOKEN", host_env_var="MY_HOST"
)
assert provider.get_token() == SAMPLE_TOKEN
assert provider.get_host() == SAMPLE_HOST
def test_invalidate_refreshes_token_from_env(self):
"""Test that invalidate re-reads token from environment."""
refreshed_token = "dapi_refreshed_token_456"
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": SAMPLE_TOKEN, "DATABRICKS_HOST": SAMPLE_HOST},
):
provider = EnvironmentCredentialProvider()
assert provider.get_token() == SAMPLE_TOKEN
# Simulate external token refresh
os.environ["DATABRICKS_TOKEN"] = refreshed_token
provider.invalidate()
assert provider.get_token() == refreshed_token
def test_invalidate_keeps_token_if_env_unset(self):
"""Test that invalidate keeps existing token if env var is unset."""
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": SAMPLE_TOKEN, "DATABRICKS_HOST": SAMPLE_HOST},
):
provider = EnvironmentCredentialProvider()
# Remove env var after initialization
del os.environ["DATABRICKS_TOKEN"]
provider.invalidate()
# Should keep the old token rather than failing
assert provider.get_token() == SAMPLE_TOKEN
class TestDatabricksTableCredentialConfig:
"""Tests for DatabricksTableCredentialConfig and resolve_credential_provider."""
def test_resolve_with_explicit_provider(self):
"""Test that explicit credential_provider is returned as-is."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
config = DatabricksTableCredentialConfig(credential_provider=provider)
result = resolve_credential_provider(config)
assert result is provider
@pytest.mark.parametrize("credential_provider_arg", [None, "no_arg"])
def test_resolve_with_none_returns_environment_provider(
self, credential_provider_arg
):
"""Test that EnvironmentCredentialProvider is returned when none provided."""
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": SAMPLE_TOKEN, "DATABRICKS_HOST": SAMPLE_HOST},
):
if credential_provider_arg == "no_arg":
config = DatabricksTableCredentialConfig()
else:
config = DatabricksTableCredentialConfig(
credential_provider=credential_provider_arg
)
result = resolve_credential_provider(config)
assert isinstance(result, EnvironmentCredentialProvider)
class TestUnityCatalogCredentialConfig:
"""Tests for UnityCatalogCredentialConfig and resolve_credential_provider."""
def test_resolve_with_explicit_provider(self):
"""Test that explicit credential_provider is returned as-is."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
config = UnityCatalogCredentialConfig(credential_provider=provider)
result = resolve_credential_provider(config)
assert result is provider
def test_resolve_with_explicit_provider_ignores_url_and_token(self):
"""Test that url/token are ignored when credential_provider is given."""
provider = StaticCredentialProvider(token=SAMPLE_TOKEN, host=SAMPLE_HOST)
config = UnityCatalogCredentialConfig(
credential_provider=provider, url=ALT_HOST, token=ALT_TOKEN
)
result = resolve_credential_provider(config)
assert result is provider
def test_resolve_with_url_and_token(self):
"""Test that url and token create a StaticCredentialProvider."""
config = UnityCatalogCredentialConfig(url=SAMPLE_URL, token=SAMPLE_TOKEN)
result = resolve_credential_provider(config)
assert isinstance(result, StaticCredentialProvider)
assert result.get_token() == SAMPLE_TOKEN
assert result.get_host() == SAMPLE_URL
@pytest.mark.parametrize(
"kwargs",
[
{},
{"url": SAMPLE_URL},
{"token": SAMPLE_TOKEN},
],
ids=["no_args", "only_url", "only_token"],
)
def test_config_raises_with_incomplete_args(self, kwargs):
"""Test that ValueError is raised when args are missing or incomplete."""
config = UnityCatalogCredentialConfig(**kwargs)
with pytest.raises(ValueError, match="Either 'credential_provider' or both"):
resolve_credential_provider(config)
@pytest.mark.parametrize(
"url,token",
[
("", SAMPLE_TOKEN),
(SAMPLE_URL, ""),
],
ids=["empty_url", "empty_token"],
)
def test_resolve_with_empty_string_raises(self, url, token):
"""Test that empty strings for url or token raise ValueError."""
config = UnityCatalogCredentialConfig(url=url, token=token)
with pytest.raises(ValueError):
resolve_credential_provider(config)
class TestCredentialProviderSerialization:
"""Tests for credential provider serialization (needed for Ray workers)."""
@pytest.mark.parametrize(
"provider_type,expected_token,expected_host",
[
("static", SAMPLE_TOKEN, SAMPLE_HOST),
("environment", SAMPLE_TOKEN, SAMPLE_HOST),
],
)
def test_provider_is_picklable(self, provider_type, expected_token, expected_host):
"""Verify credential providers can be pickled and unpickled."""
import pickle
with mock.patch.dict(
os.environ,
{"DATABRICKS_TOKEN": expected_token, "DATABRICKS_HOST": expected_host},
):
if provider_type == "static":
provider = StaticCredentialProvider(
token=expected_token, host=expected_host
)
else:
provider = EnvironmentCredentialProvider()
pickled = pickle.dumps(provider)
unpickled = pickle.loads(pickled)
assert unpickled.get_token() == expected_token
assert unpickled.get_host() == expected_host
if __name__ == "__main__":
import sys
sys.exit(pytest.main(["-v", __file__]))