Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Client Advisor | Backend test cases added #106

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
51 changes: 51 additions & 0 deletions .github/workflows/test_client_advisor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Unit Tests - Client Advisor

on:
push:
branches: [main, dev]
# Trigger on changes in these specific paths
paths:
- 'ClientAdvisor/**'
pull_request:
branches: [main, dev]
types:
- opened
- ready_for_review
- reopened
- synchronize
paths:
- 'ClientAdvisor/**'

jobs:
test_client_advisor:

name: Client Advisor Tests
runs-on: ubuntu-latest
# The if condition ensures that this job only runs if changes are in the ClientAdvisor folder

steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Backend Dependencies
run: |
cd ClientAdvisor/App
python -m pip install -r requirements.txt
python -m pip install coverage pytest-cov
- name: Run Backend Tests with Coverage
run: |
cd ClientAdvisor/App
python -m pytest -vv --cov=. --cov-report=xml --cov-report=html --cov-report=term-missing --cov-fail-under=80 --junitxml=coverage-junit.xml
- uses: actions/upload-artifact@v4
with:
name: client-advisor-coverage
path: |
ClientAdvisor/App/coverage.xml
ClientAdvisor/App/coverage-junit.xml
ClientAdvisor/App/htmlcov/
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
2 changes: 1 addition & 1 deletion ClientAdvisor/App/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ def create_app():
app.config["TEMPLATES_AUTO_RELOAD"] = True
# app.secret_key = secrets.token_hex(16)
# app.session_interface = SecureCookieSessionInterface()
# print(app.secret_key)
return app


Expand Down Expand Up @@ -1593,6 +1592,7 @@ def get_users():
cursor = conn.cursor()
cursor.execute("""select DATEDIFF(d,CAST(max(StartTime) AS Date),CAST(GETDATE() AS Date)) + 3 as ndays from ClientMeetings""")
rows = cursor.fetchall()
ndays = 0
for row in rows:
ndays = row['ndays']
sql_stmt1 = f'UPDATE ClientMeetings SET StartTime = dateadd(day,{ndays},StartTime), EndTime = dateadd(day,{ndays},EndTime)'
Expand Down
5 changes: 4 additions & 1 deletion ClientAdvisor/App/requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ azure-search-documents==11.4.0b6
azure-storage-blob==12.17.0
python-dotenv==1.0.0
azure-cosmos==4.5.0
quart==0.19.4
quart==0.19.9
uvicorn==0.24.0
aiohttp==3.9.2
gunicorn==20.1.0
quart-session==3.0.0
pymssql==2.3.0
httpx==0.27.0
pytest-asyncio==0.24.0
pytest-cov==5.0.0
isort==5.13.2
8 changes: 7 additions & 1 deletion ClientAdvisor/App/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ azure-search-documents==11.4.0b6
azure-storage-blob==12.17.0
python-dotenv==1.0.0
azure-cosmos==4.5.0
quart==0.19.4
quart==0.19.9
uvicorn==0.24.0
aiohttp==3.9.2
gunicorn==20.1.0
quart-session==3.0.0
pymssql==2.3.0
httpx==0.27.0
pytest-asyncio==0.24.0
pytest-cov==5.0.0
flake8==7.1.1
black==24.8.0
autoflake==2.3.1
isort==5.13.2
6 changes: 0 additions & 6 deletions ClientAdvisor/App/test_app.py

This file was deleted.

66 changes: 66 additions & 0 deletions ClientAdvisor/App/tests/backend/auth/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import base64
import json
from unittest.mock import patch

from backend.auth.auth_utils import (get_authenticated_user_details,
get_tenantid)


def test_get_authenticated_user_details_no_principal_id():
request_headers = {}
sample_user_data = {
"X-Ms-Client-Principal-Id": "default-id",
"X-Ms-Client-Principal-Name": "default-name",
"X-Ms-Client-Principal-Idp": "default-idp",
"X-Ms-Token-Aad-Id-Token": "default-token",
"X-Ms-Client-Principal": "default-b64",
}
with patch("backend.auth.sample_user.sample_user", sample_user_data):
user_details = get_authenticated_user_details(request_headers)
assert user_details["user_principal_id"] == "default-id"
assert user_details["user_name"] == "default-name"
assert user_details["auth_provider"] == "default-idp"
assert user_details["auth_token"] == "default-token"
assert user_details["client_principal_b64"] == "default-b64"


def test_get_authenticated_user_details_with_principal_id():
request_headers = {
"X-Ms-Client-Principal-Id": "test-id",
"X-Ms-Client-Principal-Name": "test-name",
"X-Ms-Client-Principal-Idp": "test-idp",
"X-Ms-Token-Aad-Id-Token": "test-token",
"X-Ms-Client-Principal": "test-b64",
}
user_details = get_authenticated_user_details(request_headers)
assert user_details["user_principal_id"] == "test-id"
assert user_details["user_name"] == "test-name"
assert user_details["auth_provider"] == "test-idp"
assert user_details["auth_token"] == "test-token"
assert user_details["client_principal_b64"] == "test-b64"


def test_get_tenantid_valid_b64():
user_info = {"tid": "test-tenant-id"}
client_principal_b64 = base64.b64encode(
json.dumps(user_info).encode("utf-8")
).decode("utf-8")
tenant_id = get_tenantid(client_principal_b64)
assert tenant_id == "test-tenant-id"


def test_get_tenantid_invalid_b64():
client_principal_b64 = "invalid-b64"
with patch("backend.auth.auth_utils.logging") as mock_logging:
tenant_id = get_tenantid(client_principal_b64)
assert tenant_id == ""
mock_logging.exception.assert_called_once()


def test_get_tenantid_no_tid():
user_info = {"some_other_key": "value"}
client_principal_b64 = base64.b64encode(
json.dumps(user_info).encode("utf-8")
).decode("utf-8")
tenant_id = get_tenantid(client_principal_b64)
assert tenant_id is None
184 changes: 184 additions & 0 deletions ClientAdvisor/App/tests/backend/history/test_cosmosdb_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from azure.cosmos import exceptions

from backend.history.cosmosdbservice import CosmosConversationClient


# Helper function to create an async iterable
class AsyncIterator:
def __init__(self, items):
self.items = items
self.index = 0

def __aiter__(self):
return self

async def __anext__(self):
if self.index < len(self.items):
item = self.items[self.index]
self.index += 1
return item
else:
raise StopAsyncIteration


@pytest.fixture
def cosmos_client():
return CosmosConversationClient(
cosmosdb_endpoint="https://fake.endpoint",
credential="fake_credential",
database_name="test_db",
container_name="test_container",
)


@pytest.mark.asyncio
async def test_init_invalid_credentials():
with patch(
"azure.cosmos.aio.CosmosClient.__init__",
side_effect=exceptions.CosmosHttpResponseError(
status_code=401, message="Unauthorized"
),
):
with pytest.raises(ValueError, match="Invalid credentials"):
CosmosConversationClient(
cosmosdb_endpoint="https://fake.endpoint",
credential="fake_credential",
database_name="test_db",
container_name="test_container",
)


@pytest.mark.asyncio
async def test_init_invalid_endpoint():
with patch(
"azure.cosmos.aio.CosmosClient.__init__",
side_effect=exceptions.CosmosHttpResponseError(
status_code=404, message="Not Found"
),
):
with pytest.raises(ValueError, match="Invalid CosmosDB endpoint"):
CosmosConversationClient(
cosmosdb_endpoint="https://fake.endpoint",
credential="fake_credential",
database_name="test_db",
container_name="test_container",
)


@pytest.mark.asyncio
async def test_ensure_success(cosmos_client):
cosmos_client.database_client.read = AsyncMock()
cosmos_client.container_client.read = AsyncMock()
success, message = await cosmos_client.ensure()
assert success
assert message == "CosmosDB client initialized successfully"


@pytest.mark.asyncio
async def test_ensure_failure(cosmos_client):
cosmos_client.database_client.read = AsyncMock(side_effect=Exception)
success, message = await cosmos_client.ensure()
assert not success
assert "CosmosDB database" in message


@pytest.mark.asyncio
async def test_create_conversation(cosmos_client):
cosmos_client.container_client.upsert_item = AsyncMock(return_value={"id": "123"})
response = await cosmos_client.create_conversation("user_1", "Test Conversation")
assert response["id"] == "123"


@pytest.mark.asyncio
async def test_create_conversation_failure(cosmos_client):
cosmos_client.container_client.upsert_item = AsyncMock(return_value=None)
response = await cosmos_client.create_conversation("user_1", "Test Conversation")
assert not response


@pytest.mark.asyncio
async def test_upsert_conversation(cosmos_client):
cosmos_client.container_client.upsert_item = AsyncMock(return_value={"id": "123"})
response = await cosmos_client.upsert_conversation({"id": "123"})
assert response["id"] == "123"


@pytest.mark.asyncio
async def test_delete_conversation(cosmos_client):
cosmos_client.container_client.read_item = AsyncMock(return_value={"id": "123"})
cosmos_client.container_client.delete_item = AsyncMock(return_value=True)
response = await cosmos_client.delete_conversation("user_1", "123")
assert response


@pytest.mark.asyncio
async def test_delete_conversation_not_found(cosmos_client):
cosmos_client.container_client.read_item = AsyncMock(return_value=None)
response = await cosmos_client.delete_conversation("user_1", "123")
assert response


@pytest.mark.asyncio
async def test_delete_messages(cosmos_client):
cosmos_client.get_messages = AsyncMock(
return_value=[{"id": "msg_1"}, {"id": "msg_2"}]
)
cosmos_client.container_client.delete_item = AsyncMock(return_value=True)
response = await cosmos_client.delete_messages("conv_1", "user_1")
assert len(response) == 2


@pytest.mark.asyncio
async def test_get_conversations(cosmos_client):
items = [{"id": "conv_1"}, {"id": "conv_2"}]
cosmos_client.container_client.query_items = MagicMock(
return_value=AsyncIterator(items)
)
response = await cosmos_client.get_conversations("user_1", 10)
assert len(response) == 2
assert response[0]["id"] == "conv_1"
assert response[1]["id"] == "conv_2"


@pytest.mark.asyncio
async def test_get_conversation(cosmos_client):
items = [{"id": "conv_1"}]
cosmos_client.container_client.query_items = MagicMock(
return_value=AsyncIterator(items)
)
response = await cosmos_client.get_conversation("user_1", "conv_1")
assert response["id"] == "conv_1"


@pytest.mark.asyncio
async def test_create_message(cosmos_client):
cosmos_client.container_client.upsert_item = AsyncMock(return_value={"id": "msg_1"})
cosmos_client.get_conversation = AsyncMock(return_value={"id": "conv_1"})
cosmos_client.upsert_conversation = AsyncMock()
response = await cosmos_client.create_message(
"msg_1", "conv_1", "user_1", {"role": "user", "content": "Hello"}
)
assert response["id"] == "msg_1"


@pytest.mark.asyncio
async def test_update_message_feedback(cosmos_client):
cosmos_client.container_client.read_item = AsyncMock(return_value={"id": "msg_1"})
cosmos_client.container_client.upsert_item = AsyncMock(return_value={"id": "msg_1"})
response = await cosmos_client.update_message_feedback(
"user_1", "msg_1", "positive"
)
assert response["id"] == "msg_1"


@pytest.mark.asyncio
async def test_get_messages(cosmos_client):
items = [{"id": "msg_1"}, {"id": "msg_2"}]
cosmos_client.container_client.query_items = MagicMock(
return_value=AsyncIterator(items)
)
response = await cosmos_client.get_messages("user_1", "conv_1")
assert len(response) == 2
Loading
Loading