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
5 changes: 4 additions & 1 deletion cogsol/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from jwt import decode

from cogsol.core.constants import (
get_auth_scope_id,
get_cognitive_api_base_url,
get_content_api_base_url,
)
Expand Down Expand Up @@ -133,6 +134,8 @@ def _refresh_bearer_token(self) -> None:
client_id = os.environ.get("COGSOL_AUTH_CLIENT_ID")
client_secret = os.environ.get("COGSOL_AUTH_SECRET")

scope_id = get_auth_scope_id()

if not client_secret:
raise CogSolAPIError(
"Missing authentication configuration: COGSOL_AUTH_SECRET is not set.\n"
Expand All @@ -141,7 +144,7 @@ def _refresh_bearer_token(self) -> None:
)

authority = "https://pyxiscognitivesweden.b2clogin.com/pyxiscognitivesweden.onmicrosoft.com/B2C_1A_CS_signup_signin_Sweden_MigrationOIDC"
scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{client_id}/.default"]
scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{scope_id}/.default"]

app = msal.ConfidentialClientApplication(
client_id,
Expand Down
22 changes: 22 additions & 0 deletions cogsol/core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
COGSOL_ENV_VAR: Final = "COGSOL_ENV"
COGSOL_API_BASE_VAR: Final = "COGSOL_API_BASE"
COGSOL_CONTENT_API_BASE_VAR: Final = "COGSOL_CONTENT_API_BASE"
COGSOL_AUTH_SCOPE_ID_VAR: Final = "COGSOL_AUTH_SCOPE_ID"

if os.environ.get("COGSOL_AUTH_CLIENT_ID"):
_implantation_cognitive_api_url = "https://apis-imp.cogsol.ai/cognitive"
Expand Down Expand Up @@ -63,6 +64,24 @@ def get_content_api_base_url() -> str:
return _get_env_var(COGSOL_CONTENT_API_BASE_VAR) or get_default_content_api_base_url()


AUTH_SCOPE_IDS: Final[dict[str, str]] = {
"implantation": "9efa4bc6-2b2b-4208-8c88-7a218c7061d6",
"production": "92c0d1cc-127b-4ec5-9be4-960c13c7aecc",
}


def get_default_auth_scope_id() -> str:
"""Return the default OAuth scope ID based on COGSOL_ENV."""
if get_cogsol_env() == "production":
return AUTH_SCOPE_IDS["production"]
return AUTH_SCOPE_IDS["implantation"]


def get_auth_scope_id() -> str:
"""Resolve the OAuth scope ID using env overrides when present."""
return _get_env_var(COGSOL_AUTH_SCOPE_ID_VAR) or get_default_auth_scope_id()


__all__ = [
"COGSOL_ENV_VAR",
"COGSOL_API_BASE_VAR",
Expand All @@ -76,4 +95,7 @@ def get_content_api_base_url() -> str:
"get_default_content_api_base_url",
"get_cognitive_api_base_url",
"get_content_api_base_url",
"AUTH_SCOPE_IDS",
"get_default_auth_scope_id",
"get_auth_scope_id",
]
44 changes: 44 additions & 0 deletions tests/test_api_key_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Tests for API key and credential error messages (branch: csp-1666-api-key-error-msg).

Covers:
- get_auth_scope_id: scope resolved from COGSOL_ENV, overridable via COGSOL_AUTH_SCOPE_ID.
- CogSolClient._refresh_bearer_token: detailed error when COGSOL_AUTH_SECRET is missing.
- migrate Command: no-credentials check shows helpful message and returns 1.
- importagent Command: no-credentials check shows helpful message and returns 1.
Expand Down Expand Up @@ -38,19 +39,59 @@ def _bare_client() -> CogSolClient:
# ---------------------------------------------------------------------------


class TestAuthScopeIdResolution:
"""get_auth_scope_id must return the correct scope based on COGSOL_ENV,
defaulting to the implantation scope for missing or unknown values.
COGSOL_AUTH_SCOPE_ID overrides the derived value when set."""

def test_returns_implantation_scope_when_env_not_set(self, monkeypatch):
monkeypatch.delenv("COGSOL_ENV", raising=False)
monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False)

from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id

assert get_auth_scope_id() == AUTH_SCOPE_IDS["implantation"]

def test_returns_implantation_scope_when_env_is_unknown(self, monkeypatch):
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False)

from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id

assert get_auth_scope_id() == AUTH_SCOPE_IDS["implantation"]

def test_returns_production_scope_when_env_is_production(self, monkeypatch):
monkeypatch.setenv("COGSOL_ENV", "production")
monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False)

from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id

assert get_auth_scope_id() == AUTH_SCOPE_IDS["production"]

def test_scope_id_env_var_overrides_derived_value(self, monkeypatch):
monkeypatch.setenv("COGSOL_AUTH_SCOPE_ID", "custom-scope-id-override")
monkeypatch.setenv("COGSOL_ENV", "production")

from cogsol.core.constants import get_auth_scope_id

assert get_auth_scope_id() == "custom-scope-id-override"


class TestMissingAuthSecret:
"""_refresh_bearer_token must raise CogSolAPIError with a helpful message
when COGSOL_AUTH_CLIENT_ID is set but COGSOL_AUTH_SECRET is missing."""

def test_raises_cogsol_api_error(self, monkeypatch):
monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id")
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False)

with pytest.raises(CogSolAPIError):
_bare_client()._refresh_bearer_token()

def test_error_mentions_missing_secret_var(self, monkeypatch):
monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id")
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False)

with pytest.raises(CogSolAPIError) as exc_info:
Expand All @@ -60,6 +101,7 @@ def test_error_mentions_missing_secret_var(self, monkeypatch):

def test_error_includes_onboarding_url(self, monkeypatch):
monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id")
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False)

with pytest.raises(CogSolAPIError) as exc_info:
Expand All @@ -69,6 +111,7 @@ def test_error_includes_onboarding_url(self, monkeypatch):

def test_error_mentions_implantation_portal(self, monkeypatch):
monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id")
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False)

with pytest.raises(CogSolAPIError) as exc_info:
Expand All @@ -80,6 +123,7 @@ def test_no_error_when_secret_is_present(self, monkeypatch):
"""With a valid secret the error must NOT be raised (msal call may fail,
but that is a different error path)."""
monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id")
monkeypatch.setenv("COGSOL_ENV", "development")
monkeypatch.setenv("COGSOL_AUTH_SECRET", "some-secret")

# The error about missing secret should not be raised;
Expand Down
Loading