diff --git a/README.md b/README.md index 3fd7cee..7647848 100644 --- a/README.md +++ b/README.md @@ -118,11 +118,13 @@ agents/salesagent/ ### 3. Configure Environment -Copy `.env.example` to `.env` and set your API credentials: +To use CogSol you need API credentials. Visit **[https://onboarding.cogsol.ai](https://onboarding.cogsol.ai)** to obtain your credentials. Make sure to also configure the service API key in the implantation portal before running any commands. + +Copy `.env.example` to `.env` and fill in the credentials obtained from the portal: ```env COGSOL_ENV=development -COGSOL_API_KEY=your-api-key +COGSOL_API_KEY=your-api-key # Obtain from https://onboarding.cogsol.ai # Optional: Azure AD B2C client credentials for JWT # If not provided, the Auth will be skipped COGSOL_AUTH_CLIENT_ID=your-client-id @@ -666,17 +668,14 @@ from cogsol.content import BaseRetrieval, ReorderingStrategy ### Environment Variables +Credentials are obtained from the CogSol onboarding portal at **[https://onboarding.cogsol.ai](https://onboarding.cogsol.ai)**. Configure your service API key there before running migrations or using the CLI. + | Variable | Required | Description | |----------|----------|-------------| -| `COGSOL_API_KEY` | Yes | API Key authentication | +| `COGSOL_API_KEY` | Yes | Service API key — obtain from the portal at https://onboarding.cogsol.ai | | `COGSOL_ENV` | No | Environment name (e.g., `local`, `development`, `production`) | -| `COGSOL_AUTH_CLIENT_ID` | No | Client Id provided for adminitrators | -| `COGSOL_AUTH_SECRET` | No | Auth Secret provided for adminitrators | - -# Optional: Azure AD B2C client credentials for JWT\ -# If not provided, the Auth will be skipped -COGSOL_AUTH_CLIENT_ID=your-client-id -COGSOL_AUTH_SECRET=your-client-secret +| `COGSOL_AUTH_CLIENT_ID` | No | Client Id provided for administrators | +| `COGSOL_AUTH_SECRET` | No | Auth secret provided for administrators | ### Project Settings (`settings.py`) diff --git a/cogsol/core/api.py b/cogsol/core/api.py index f8347e8..eda7098 100644 --- a/cogsol/core/api.py +++ b/cogsol/core/api.py @@ -134,7 +134,11 @@ def _refresh_bearer_token(self) -> None: client_secret = os.environ.get("COGSOL_AUTH_SECRET") if not client_secret: - raise CogSolAPIError("Missing authentication configuration: COGSOL_AUTH_SECRET") + raise CogSolAPIError( + "Missing authentication configuration: COGSOL_AUTH_SECRET is not set.\n" + "To obtain your credentials, visit https://onboarding.cogsol.ai\n" + "and configure the service API key in the implantation portal." + ) authority = "https://pyxiscognitivesweden.b2clogin.com/pyxiscognitivesweden.onmicrosoft.com/B2C_1A_CS_signup_signin_Sweden_MigrationOIDC" scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{client_id}/.default"] diff --git a/cogsol/management/commands/chat.py b/cogsol/management/commands/chat.py index 6c3266c..9f06894 100644 --- a/cogsol/management/commands/chat.py +++ b/cogsol/management/commands/chat.py @@ -319,8 +319,16 @@ def handle(self, project_path: Path | None, **options: Any) -> int: api_base = get_cognitive_api_base_url() api_key = os.environ.get("COGSOL_API_KEY") - if not api_base: - print_error("COGSOL_API_BASE is required in .env to chat with CogSol.") + if not api_key and not os.environ.get("COGSOL_AUTH_CLIENT_ID"): + print_error( + "No API credentials found.\n" + "Set COGSOL_API_KEY in your .env file to authenticate with the CogSol API.\n" + "\n" + "To obtain your credentials:\n" + " 1. Visit https://onboarding.cogsol.ai\n" + " 2. Configure the service API key in the implantation portal\n" + " 3. Copy the key to COGSOL_API_KEY in your .env file" + ) return 1 remote_ids = self._load_remote_ids(project_path, app) diff --git a/cogsol/management/commands/importagent.py b/cogsol/management/commands/importagent.py index 17c7736..1a7a6ee 100644 --- a/cogsol/management/commands/importagent.py +++ b/cogsol/management/commands/importagent.py @@ -296,8 +296,16 @@ def handle(self, project_path: Path | None, **options: Any) -> int: api_base = get_cognitive_api_base_url() api_key = os.environ.get("COGSOL_API_KEY") content_base = get_content_api_base_url() or api_base - if not api_base: - print("COGSOL_API_BASE is required in .env to import.") + if not api_key and not os.environ.get("COGSOL_AUTH_CLIENT_ID"): + print( + "Error: No API credentials found.\n" + "Set COGSOL_API_KEY in your .env file to authenticate with the CogSol API.\n" + "\n" + "To obtain your credentials:\n" + " 1. Visit https://onboarding.cogsol.ai\n" + " 2. Configure the service API key in the implantation portal\n" + " 3. Copy the key to COGSOL_API_KEY in your .env file" + ) return 1 client = CogSolClient(api_base, api_key=api_key, content_base_url=content_base) diff --git a/cogsol/management/commands/migrate.py b/cogsol/management/commands/migrate.py index 41353a1..e37c947 100644 --- a/cogsol/management/commands/migrate.py +++ b/cogsol/management/commands/migrate.py @@ -58,8 +58,16 @@ def handle(self, project_path: Path | None, **options: Any) -> int: api_base = get_cognitive_api_base_url() api_key = self._env("COGSOL_API_KEY", required=False) content_base = get_content_api_base_url() - if not api_base: - print("COGSOL_API_BASE is required in .env to run migrations against CogSol APIs.") + if not api_key and not os.environ.get("COGSOL_AUTH_CLIENT_ID"): + print( + "Error: No API credentials found.\n" + "Set COGSOL_API_KEY in your .env file to authenticate with the CogSol API.\n" + "\n" + "To obtain your credentials:\n" + " 1. Visit https://onboarding.cogsol.ai\n" + " 2. Configure the service API key in the implantation portal\n" + " 3. Copy the key to COGSOL_API_KEY in your .env file" + ) return 1 exit_code = 0 diff --git a/tests/test_api_key_errors.py b/tests/test_api_key_errors.py new file mode 100644 index 0000000..461f3ec --- /dev/null +++ b/tests/test_api_key_errors.py @@ -0,0 +1,293 @@ +""" +Tests for API key and credential error messages (branch: csp-1666-api-key-error-msg). + +Covers: +- 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. +- chat Command: no-credentials check shows helpful message and returns 1. +- Positive cases: having COGSOL_API_KEY or COGSOL_AUTH_CLIENT_ID bypasses the check. +""" + +from __future__ import annotations + +from unittest.mock import patch + +import pytest + +from cogsol.core.api import CogSolAPIError, CogSolClient + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _bare_client() -> CogSolClient: + """Instantiate CogSolClient without triggering __init__ network calls.""" + client = CogSolClient.__new__(CogSolClient) + client.bearer_token = None + client.bearer_token_expires_at = None + client.api_key = None + client.base_url = "https://fake.api.example.com" + client.content_base_url = None + return client + + +# --------------------------------------------------------------------------- +# CogSolClient._refresh_bearer_token +# --------------------------------------------------------------------------- + + +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.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.delenv("COGSOL_AUTH_SECRET", raising=False) + + with pytest.raises(CogSolAPIError) as exc_info: + _bare_client()._refresh_bearer_token() + + assert "COGSOL_AUTH_SECRET is not set" in str(exc_info.value) + + def test_error_includes_onboarding_url(self, monkeypatch): + monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) + + with pytest.raises(CogSolAPIError) as exc_info: + _bare_client()._refresh_bearer_token() + + assert "https://onboarding.cogsol.ai" in str(exc_info.value) + + def test_error_mentions_implantation_portal(self, monkeypatch): + monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) + + with pytest.raises(CogSolAPIError) as exc_info: + _bare_client()._refresh_bearer_token() + + assert "implantation portal" in str(exc_info.value) + + 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_AUTH_SECRET", "some-secret") + + # The error about missing secret should not be raised; + # msal.ConfidentialClientApplication is mocked to avoid network calls. + with patch("cogsol.core.api.msal.ConfidentialClientApplication") as mock_app: + mock_app.return_value.acquire_token_for_client.return_value = { + "access_token": "fake-token" + } + # Should not raise CogSolAPIError about missing secret + client = _bare_client() + client._refresh_bearer_token() + assert client.bearer_token == "fake-token" + + +# --------------------------------------------------------------------------- +# migrate Command – credential check +# --------------------------------------------------------------------------- + + +class TestMigrateCommandCredentialCheck: + """migrate handle() must show a helpful error and return 1 when no credentials + are available, and must proceed normally when credentials are present.""" + + def test_no_credentials_returns_1(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + result = Command().handle(project_path=tmp_path) + + assert result == 1 + + def test_no_credentials_prints_no_credentials_message(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + Command().handle(project_path=tmp_path) + + out = capsys.readouterr().out + assert "No API credentials found" in out + + def test_no_credentials_prints_api_key_instruction(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + Command().handle(project_path=tmp_path) + + out = capsys.readouterr().out + assert "COGSOL_API_KEY" in out + assert "https://onboarding.cogsol.ai" in out + + def test_no_credentials_prints_implantation_portal(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + Command().handle(project_path=tmp_path) + + out = capsys.readouterr().out + assert "implantation portal" in out + + def test_api_key_passes_credential_check(self, tmp_path, monkeypatch, capsys): + """When COGSOL_API_KEY is set the command should pass the credentials check. + It will still return 1 because there are no migration folders in tmp_path, + but the credentials error must NOT be shown.""" + monkeypatch.setenv("COGSOL_API_KEY", "test-key-abc") + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + Command().handle(project_path=tmp_path) + + out = capsys.readouterr().out + assert "No API credentials found" not in out + # Confirms the command progressed past the check + assert "No migrations folder found" in out + + def test_auth_client_id_passes_credential_check(self, tmp_path, monkeypatch, capsys): + """When COGSOL_AUTH_CLIENT_ID is set the credential check should pass. + It will still return 1 because there are no migration folders in tmp_path, + but the credentials error must NOT be shown.""" + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.migrate import Command + + with patch("cogsol.management.commands.migrate.load_dotenv"): + Command().handle(project_path=tmp_path) + + out = capsys.readouterr().out + assert "No API credentials found" not in out + # Confirms the command progressed past the check + assert "No migrations folder found" in out + + +# --------------------------------------------------------------------------- +# importagent Command – credential check +# --------------------------------------------------------------------------- + + +class TestImportagentCommandCredentialCheck: + """importagent handle() must show a helpful error and return 1 when no + credentials are available.""" + + def test_no_credentials_returns_1(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.importagent import Command + + with patch("cogsol.management.commands.importagent.load_dotenv"): + result = Command().handle(project_path=tmp_path, assistant_id=1, app="agents") + + assert result == 1 + + def test_no_credentials_prints_helpful_message(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.importagent import Command + + with patch("cogsol.management.commands.importagent.load_dotenv"): + Command().handle(project_path=tmp_path, assistant_id=1, app="agents") + + out = capsys.readouterr().out + assert "No API credentials found" in out + assert "COGSOL_API_KEY" in out + assert "https://onboarding.cogsol.ai" in out + + def test_no_credentials_prints_implantation_portal(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.importagent import Command + + with patch("cogsol.management.commands.importagent.load_dotenv"): + Command().handle(project_path=tmp_path, assistant_id=1, app="agents") + + out = capsys.readouterr().out + assert "implantation portal" in out + + +# --------------------------------------------------------------------------- +# chat Command – credential check +# --------------------------------------------------------------------------- + + +class TestChatCommandCredentialCheck: + """chat handle() must show a helpful error and return 1 when no credentials + are available. chat uses print_error() which writes to stdout with ANSI codes; + the plain text content must still be present.""" + + def test_no_credentials_returns_1(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.chat import Command + + with patch("cogsol.management.commands.chat.load_dotenv"): + result = Command().handle(project_path=tmp_path, agent="my-agent", app="agents") + + assert result == 1 + + def test_no_credentials_prints_helpful_message(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.chat import Command + + with patch("cogsol.management.commands.chat.load_dotenv"): + Command().handle(project_path=tmp_path, agent="my-agent", app="agents") + + out = capsys.readouterr().out + assert "No API credentials found" in out + assert "COGSOL_API_KEY" in out + assert "https://onboarding.cogsol.ai" in out + + def test_no_credentials_prints_implantation_portal(self, tmp_path, monkeypatch, capsys): + monkeypatch.delenv("COGSOL_API_KEY", raising=False) + monkeypatch.delenv("COGSOL_AUTH_CLIENT_ID", raising=False) + (tmp_path / ".env").write_text("") + + from cogsol.management.commands.chat import Command + + with patch("cogsol.management.commands.chat.load_dotenv"): + Command().handle(project_path=tmp_path, agent="my-agent", app="agents") + + out = capsys.readouterr().out + assert "implantation portal" in out