Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changes/unreleased/breaking-20260329-150739.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kind: breaking
body: Remove default credential fallback; explicit token credential is now required
time: 2026-03-29T15:07:39.9569937+03:00
custom:
Author: shirasassoon
AuthorLink: https://github.com/shirasassoon
Issue: "909"
IssueLink: https://github.com/microsoft/fabric-cicd/issues/909
13 changes: 6 additions & 7 deletions src/fabric_cicd/fabric_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(
environment: str = "N/A",
workspace_id: Optional[str] = None,
workspace_name: Optional[str] = None,
token_credential: TokenCredential = None,
token_credential: Optional[TokenCredential] = None,
**kwargs,
) -> None:
"""
Expand All @@ -47,7 +47,7 @@ def __init__(
repository_directory: Local directory path of the repository where items are to be deployed from.
item_type_in_scope: Item types that should be deployed for a given workspace. If omitted, defaults to all available item types.
environment: The environment to be used for parameterization.
token_credential: The token credential to use for API requests.
token_credential: The token credential to use for API requests (e.g., AzureCliCredential, ClientSecretCredential).
kwargs: Additional keyword arguments.

Examples:
Expand Down Expand Up @@ -102,12 +102,11 @@ def __init__(

if token_credential is None:
if _is_fabric_runtime():
token_credential = _generate_fabric_credential()
token_credential = validate_token_credential(_generate_fabric_credential())
logger.debug("Running in Fabric runtime - using generated Fabric credential for authentication.")
else:
# if credential is not defined, use DefaultAzureCredential
from azure.identity import DefaultAzureCredential

token_credential = DefaultAzureCredential()
msg = "A TokenCredential is required to authenticate API requests. Please pass a 'token_credential' (e.g., AzureCliCredential, ClientSecretCredential)."
raise InputError(msg, logger)
else:
token_credential = validate_token_credential(token_credential)

Expand Down
30 changes: 30 additions & 0 deletions tests/test_fabric_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import pytest
import yaml
from fixtures.credentials import DummyTokenCredential

from fabric_cicd.fabric_workspace import FabricWorkspace, constants

Expand Down Expand Up @@ -120,6 +121,7 @@ def _create_workspace(workspace_id, repository_directory, item_type_in_scope=Non
workspace_id=workspace_id,
repository_directory=repository_directory,
item_type_in_scope=item_type_in_scope,
token_credential=DummyTokenCredential(),
**kwargs,
)
# Call refresh methods to populate workspace data
Expand Down Expand Up @@ -1048,6 +1050,32 @@ def test_parameter_file_path_invalid_type_rejected(temp_workspace_dir, patched_f
assert not workspace.environment_parameter


def test_no_token_credential_outside_fabric_runtime_raises_error(temp_workspace_dir, valid_workspace_id):
"""Test that constructing FabricWorkspace without token_credential outside Fabric runtime raises InputError."""
from fabric_cicd._common._exceptions import InputError

# Create a simple platform file so directory validation passes
notebook_dir = temp_workspace_dir / "Test Notebook"
notebook_dir.mkdir()
platform_file = notebook_dir / ".platform"
platform_content = {
"metadata": {"type": "Notebook", "displayName": "Test Notebook"},
"config": {"logicalId": "12345678-1234-5678-abcd-1234567890ab"},
}
with platform_file.open("w", encoding="utf-8") as f:
json.dump(platform_content, f)

with patch("fabric_cicd.fabric_workspace._is_fabric_runtime", return_value=False):
with pytest.raises(InputError) as exc_info:
FabricWorkspace(
workspace_id=valid_workspace_id,
repository_directory=str(temp_workspace_dir),
)

assert "TokenCredential is required" in str(exc_info.value)
assert "token_credential" in str(exc_info.value)


def test_base_api_url_kwarg_raises_error(temp_workspace_dir, valid_workspace_id):
"""Test that passing base_api_url as kwarg raises an error."""
from fabric_cicd._common._exceptions import InputError
Expand All @@ -1072,6 +1100,7 @@ def test_base_api_url_kwarg_raises_error(temp_workspace_dir, valid_workspace_id)
workspace_id=valid_workspace_id,
repository_directory=str(temp_workspace_dir),
base_api_url="https://custom.api.url",
token_credential=DummyTokenCredential(),
)

# Verify the error message contains the expected text
Expand Down Expand Up @@ -1619,6 +1648,7 @@ def test_mix_of_default_and_non_default_logical_ids(temp_workspace_dir, patched_
assert workspace.repository_items["Notebook"]["Git Notebook"].logical_id == unique_logical_id
assert workspace.repository_items["DataPipeline"]["Exported Pipeline"].logical_id == constants.DEFAULT_GUID


def test_publish_variable_library_only_calls_replace_parameters(
temp_workspace_dir, patched_fabric_workspace, valid_workspace_id
):
Expand Down
24 changes: 24 additions & 0 deletions tests/test_publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from unittest.mock import MagicMock, patch

import pytest
from fixtures.credentials import DummyTokenCredential

import fabric_cicd.publish as publish
from fabric_cicd import constants
Expand Down Expand Up @@ -124,6 +125,7 @@ def test_publish_only_existing_item_types(mock_endpoint, temp_workspace_dir):
workspace = FabricWorkspace(
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(workspace)
Expand All @@ -148,6 +150,7 @@ def test_default_none_item_type_in_scope_includes_all_types(mock_endpoint, temp_
workspace = FabricWorkspace(
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
token_credential=DummyTokenCredential(),
)

expected_types = list(constants.ACCEPTED_ITEM_TYPES)
Expand All @@ -161,6 +164,7 @@ def test_empty_item_type_in_scope_list(mock_endpoint, temp_workspace_dir):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=[],
token_credential=DummyTokenCredential(),
)
assert workspace.item_type_in_scope == []

Expand All @@ -180,6 +184,7 @@ def test_invalid_item_types_in_scope(mock_endpoint, temp_workspace_dir):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["InvalidItemType"],
token_credential=DummyTokenCredential(),
)


Expand All @@ -193,6 +198,7 @@ def test_multiple_invalid_item_types_in_scope(mock_endpoint, temp_workspace_dir)
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["FakeType", "AnotherInvalidType"],
token_credential=DummyTokenCredential(),
)


Expand All @@ -206,6 +212,7 @@ def test_mixed_valid_and_invalid_item_types_in_scope(mock_endpoint, temp_workspa
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook", "BadType", "Environment"],
token_credential=DummyTokenCredential(),
)


Expand Down Expand Up @@ -245,6 +252,7 @@ def test_unpublish_feature_flag_warnings(mock_endpoint, temp_workspace_dir, capl
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Lakehouse", "Warehouse", "SQLDatabase", "Eventhouse"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace)
Expand Down Expand Up @@ -288,6 +296,7 @@ def test_unpublish_with_feature_flags_enabled(mock_endpoint, temp_workspace_dir,
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Lakehouse"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace)
Expand Down Expand Up @@ -339,6 +348,7 @@ def track_unpublish(_self, item_name, item_type):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace)
Expand Down Expand Up @@ -387,6 +397,7 @@ def track_unpublish(_self, item_name, item_type):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace, item_name_exclude_regex=r"^Protected.*")
Expand Down Expand Up @@ -436,6 +447,7 @@ def track_unpublish(_self, item_name, item_type):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace, items_to_include=["TargetOrphan.Notebook"])
Expand Down Expand Up @@ -477,6 +489,7 @@ def track_unpublish(_self, item_name, item_type):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.unpublish_all_orphan_items(workspace)
Expand Down Expand Up @@ -520,6 +533,7 @@ def mock_publish_mirroreddatabase():
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Lakehouse", "MirroredDatabase"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(workspace)
Expand Down Expand Up @@ -559,6 +573,7 @@ def test_folder_exclusion_with_regex(mock_endpoint, temp_workspace_dir):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook", "SemanticModel"],
token_credential=DummyTokenCredential(),
)

exclude_regex = r".*legacy.*"
Expand Down Expand Up @@ -593,6 +608,7 @@ def test_folder_exclusion_with_anchored_regex(mock_endpoint, temp_workspace_dir)
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

exclude_regex = r"^/legacy$"
Expand All @@ -619,6 +635,7 @@ def test_item_name_exclusion_still_works(mock_endpoint, temp_workspace_dir):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

exclude_regex = r".*DoNotPublish.*"
Expand Down Expand Up @@ -656,6 +673,7 @@ def test_folder_inclusion_with_folder_path_to_include(mock_endpoint, temp_worksp
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook", "SemanticModel"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(
Expand Down Expand Up @@ -692,6 +710,7 @@ def test_folder_inclusion_and_exclusion_together(mock_endpoint, temp_workspace_d
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

with pytest.raises(
Expand Down Expand Up @@ -719,6 +738,7 @@ def test_empty_folder_path_to_include_raises_error(mock_endpoint, temp_workspace
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

with pytest.raises(InputError, match="folder_path_to_include must not be an empty list"):
Expand Down Expand Up @@ -748,6 +768,7 @@ def test_folder_exclusion_with_items_to_include(mock_endpoint, temp_workspace_di
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(
Expand Down Expand Up @@ -778,6 +799,7 @@ def test_folder_inclusion_with_item_exclusion(mock_endpoint, temp_workspace_dir)
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(
Expand Down Expand Up @@ -808,6 +830,7 @@ def test_folder_inclusion_with_items_to_include(mock_endpoint, temp_workspace_di
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(
Expand Down Expand Up @@ -840,6 +863,7 @@ def test_all_filters_combined(mock_endpoint, temp_workspace_dir):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_workspace_dir),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)

publish.publish_all_items(
Expand Down
2 changes: 2 additions & 0 deletions tests/test_response_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from unittest.mock import MagicMock, patch

import pytest
from fixtures.credentials import DummyTokenCredential

import fabric_cicd.constants as constants
import fabric_cicd.publish as publish
Expand Down Expand Up @@ -91,6 +92,7 @@ def test_workspace_with_notebook(mock_endpoint):
workspace_id="12345678-1234-5678-abcd-1234567890ab",
repository_directory=str(temp_path),
item_type_in_scope=["Notebook"],
token_credential=DummyTokenCredential(),
)
# Manually set up repository items since we're patching the refresh methods
workspace.repository_items = {
Expand Down
3 changes: 3 additions & 0 deletions tests/test_subfolders.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from unittest.mock import MagicMock, patch

import pytest
from fixtures.credentials import DummyTokenCredential

from fabric_cicd.fabric_workspace import FabricWorkspace

Expand Down Expand Up @@ -110,6 +111,7 @@ def _create_workspace(workspace_id, repository_directory, item_type_in_scope, **
workspace_id=workspace_id,
repository_directory=repository_directory,
item_type_in_scope=item_type_in_scope,
token_credential=DummyTokenCredential(),
**kwargs,
)

Expand Down Expand Up @@ -268,6 +270,7 @@ def mock_invoke_side_effect(method, url, **_kwargs):
workspace_id=valid_workspace_id,
repository_directory=str(repository_with_subfolders),
item_type_in_scope=["Notebook", "DataPipeline"],
token_credential=DummyTokenCredential(),
)

# Call methods in the intended order to populate folder structures
Expand Down
Loading