Skip to content

Commit 80de68a

Browse files
committed
feat: add comprehensive test suite for BrainusAI client and models
1 parent 2b15f83 commit 80de68a

12 files changed

Lines changed: 793 additions & 1 deletion

File tree

.github/workflows/tests.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: ["main", "dev"]
6+
pull_request:
7+
branches: ["main", "dev"]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
matrix:
15+
python-version: ["3.11", "3.12", "3.13"]
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
25+
- name: Install dependencies
26+
run: pip install -e ".[dev]"
27+
28+
- name: Run tests
29+
run: python -m pytest tests/ -v

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ target-version = "py311"
6060
select = ["E", "F", "I", "UP", "B", "SIM", "TCH"]
6161
ignore = []
6262

63+
[tool.pytest.ini_options]
64+
asyncio_mode = "auto"
65+
testpaths = ["tests"]
66+
6367
[tool.mypy]
6468
python_version = "3.11"
6569
strict = true

src/brainus_ai/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
)
1515
from .models import QueryRequest, QueryResponse, Citation, UsageStats, Plan, QueryFilters, PlanInfo
1616

17-
__version__ = "0.1.0"
17+
__version__ = "0.1.6"
1818

1919
__all__ = [
2020
"BrainusAI",

tests/__init__.py

Whitespace-only changes.

tests/conftest.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Shared fixtures for Brainus AI SDK tests."""
2+
3+
import pytest
4+
from brainus_ai import BrainusAI
5+
6+
VALID_API_KEY = "brainus_test_key_abc123"
7+
BASE_URL = "https://api.brainus.lk"
8+
9+
10+
@pytest.fixture
11+
def api_key() -> str:
12+
return VALID_API_KEY
13+
14+
15+
@pytest.fixture
16+
def client() -> BrainusAI:
17+
return BrainusAI(api_key=VALID_API_KEY)
18+
19+
20+
# Reusable mock response payloads
21+
QUERY_RESPONSE = {
22+
"answer": "Python is a high-level programming language.",
23+
"citations": [
24+
{
25+
"document_id": "doc_001",
26+
"document_name": "Python Basics.pdf",
27+
"pages": [1, 2],
28+
"metadata": {"subject": "ICT", "grade": "12"},
29+
"chunk_text": "Python is a high-level...",
30+
}
31+
],
32+
"has_citations": True,
33+
}
34+
35+
USAGE_RESPONSE = {
36+
"total_requests": 42,
37+
"total_tokens": 8500,
38+
"total_cost_usd": 0.034,
39+
"by_endpoint": {"/api/v1/dev/query": 42},
40+
"quota_remaining": 958,
41+
"quota_percentage": 4.2,
42+
"plan": {
43+
"name": "Pro",
44+
"rate_limit_per_minute": 60,
45+
"rate_limit_per_day": 1000,
46+
"monthly_quota": 1000,
47+
},
48+
"period_start": "2026-03-01",
49+
"period_end": "2026-03-31",
50+
}
51+
52+
PLANS_RESPONSE = {
53+
"plans": [
54+
{
55+
"id": "plan_free",
56+
"name": "Free",
57+
"description": "Free tier",
58+
"rate_limit_per_minute": 10,
59+
"rate_limit_per_day": 100,
60+
"monthly_quota": 100,
61+
"price_lkr": None,
62+
"allowed_models": ["brainusai-fast"],
63+
"features": {},
64+
"is_active": True,
65+
},
66+
{
67+
"id": "plan_pro",
68+
"name": "Pro",
69+
"description": "Pro tier",
70+
"rate_limit_per_minute": 60,
71+
"rate_limit_per_day": 1000,
72+
"monthly_quota": 1000,
73+
"price_lkr": 2500.0,
74+
"allowed_models": ["brainusai-fast", "brainusai-thinking"],
75+
"features": {"priority_support": True},
76+
"is_active": True,
77+
},
78+
]
79+
}

tests/test_client.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Tests for BrainusAI client initialization."""
2+
3+
import pytest
4+
from brainus_ai import BrainusAI
5+
from brainus_ai.exceptions import AuthenticationError
6+
7+
8+
class TestClientInit:
9+
def test_valid_key_accepted(self):
10+
client = BrainusAI(api_key="brainus_abc123")
11+
assert client.api_key == "brainus_abc123"
12+
13+
def test_empty_key_raises(self):
14+
with pytest.raises(AuthenticationError):
15+
BrainusAI(api_key="")
16+
17+
def test_wrong_prefix_sk_live_raises(self):
18+
with pytest.raises(AuthenticationError):
19+
BrainusAI(api_key="sk_live_abc123")
20+
21+
def test_wrong_prefix_bare_string_raises(self):
22+
with pytest.raises(AuthenticationError):
23+
BrainusAI(api_key="my_secret_key")
24+
25+
def test_wrong_prefix_openai_style_raises(self):
26+
with pytest.raises(AuthenticationError):
27+
BrainusAI(api_key="sk-proj-abc123")
28+
29+
def test_default_base_url(self):
30+
client = BrainusAI(api_key="brainus_abc123")
31+
assert client.base_url == "https://api.brainus.lk"
32+
33+
def test_trailing_slash_stripped_from_base_url(self):
34+
client = BrainusAI(api_key="brainus_abc123", base_url="https://api.brainus.lk/")
35+
assert client.base_url == "https://api.brainus.lk"
36+
37+
def test_custom_base_url(self):
38+
client = BrainusAI(api_key="brainus_abc123", base_url="http://localhost:8000")
39+
assert client.base_url == "http://localhost:8000"
40+
41+
def test_default_timeout(self):
42+
client = BrainusAI(api_key="brainus_abc123")
43+
assert client.timeout == 30.0
44+
45+
def test_custom_timeout(self):
46+
client = BrainusAI(api_key="brainus_abc123", timeout=60.0)
47+
assert client.timeout == 60.0
48+
49+
def test_default_max_retries(self):
50+
client = BrainusAI(api_key="brainus_abc123")
51+
assert client.max_retries == 3
52+
53+
def test_custom_max_retries(self):
54+
client = BrainusAI(api_key="brainus_abc123", max_retries=5)
55+
assert client.max_retries == 5

tests/test_context_manager.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Tests for async context manager and close()."""
2+
3+
from unittest.mock import AsyncMock, patch
4+
5+
import pytest
6+
from pytest_httpx import HTTPXMock
7+
8+
from brainus_ai import BrainusAI
9+
10+
from .conftest import BASE_URL, QUERY_RESPONSE, VALID_API_KEY
11+
12+
QUERY_URL = f"{BASE_URL}/api/v1/dev/query"
13+
14+
15+
async def test_context_manager_returns_client() -> None:
16+
async with BrainusAI(api_key=VALID_API_KEY) as client:
17+
assert isinstance(client, BrainusAI)
18+
19+
20+
async def test_context_manager_closes_on_exit(httpx_mock: HTTPXMock) -> None:
21+
httpx_mock.add_response(method="GET", url=f"{BASE_URL}/api/v1/dev/usage", json={"total_requests": 0, "by_endpoint": {}})
22+
async with BrainusAI(api_key=VALID_API_KEY) as client:
23+
await client.get_usage()
24+
# After exiting, the underlying httpx client should be closed.
25+
assert client._client.is_closed
26+
27+
28+
async def test_close_marks_client_as_closed() -> None:
29+
client = BrainusAI(api_key=VALID_API_KEY)
30+
assert not client._client.is_closed
31+
await client.close()
32+
assert client._client.is_closed
33+
34+
35+
async def test_requests_work_inside_context_manager(httpx_mock: HTTPXMock) -> None:
36+
httpx_mock.add_response(method="POST", url=QUERY_URL, json=QUERY_RESPONSE)
37+
async with BrainusAI(api_key=VALID_API_KEY) as client:
38+
resp = await client.query("test")
39+
assert resp.answer == "Python is a high-level programming language."

tests/test_exceptions.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Tests for exception classes."""
2+
3+
from brainus_ai.exceptions import (
4+
APIError,
5+
AuthenticationError,
6+
BrainusError,
7+
QuotaExceededError,
8+
RateLimitError,
9+
)
10+
11+
12+
class TestBrainusError:
13+
def test_is_exception(self) -> None:
14+
assert issubclass(BrainusError, Exception)
15+
16+
def test_message_attribute(self) -> None:
17+
e = BrainusError("something broke")
18+
assert e.message == "something broke"
19+
20+
def test_str_representation(self) -> None:
21+
e = BrainusError("something broke")
22+
assert "something broke" in str(e)
23+
24+
25+
class TestAuthenticationError:
26+
def test_is_brainus_error(self) -> None:
27+
assert issubclass(AuthenticationError, BrainusError)
28+
29+
def test_message(self) -> None:
30+
e = AuthenticationError("bad key")
31+
assert e.message == "bad key"
32+
33+
34+
class TestRateLimitError:
35+
def test_is_brainus_error(self) -> None:
36+
assert issubclass(RateLimitError, BrainusError)
37+
38+
def test_retry_after_none_by_default(self) -> None:
39+
e = RateLimitError()
40+
assert e.retry_after is None
41+
42+
def test_retry_after_set(self) -> None:
43+
e = RateLimitError("limit hit", retry_after=60)
44+
assert e.retry_after == 60
45+
46+
def test_default_message(self) -> None:
47+
e = RateLimitError()
48+
assert e.message == "Rate limit exceeded"
49+
50+
def test_custom_message(self) -> None:
51+
e = RateLimitError("slow down")
52+
assert e.message == "slow down"
53+
54+
55+
class TestQuotaExceededError:
56+
def test_is_brainus_error(self) -> None:
57+
assert issubclass(QuotaExceededError, BrainusError)
58+
59+
def test_message(self) -> None:
60+
e = QuotaExceededError("quota gone")
61+
assert e.message == "quota gone"
62+
63+
64+
class TestAPIError:
65+
def test_is_brainus_error(self) -> None:
66+
assert issubclass(APIError, BrainusError)
67+
68+
def test_status_code_none_by_default(self) -> None:
69+
e = APIError("error")
70+
assert e.status_code is None
71+
72+
def test_status_code_set(self) -> None:
73+
e = APIError("server error", status_code=500)
74+
assert e.status_code == 500
75+
76+
def test_message(self) -> None:
77+
e = APIError("bad gateway", status_code=502)
78+
assert e.message == "bad gateway"
79+
80+
81+
class TestInstanceofChecks:
82+
def test_all_subclass_of_brainus_error(self) -> None:
83+
for cls in [AuthenticationError, RateLimitError, QuotaExceededError, APIError]:
84+
assert issubclass(cls, BrainusError)
85+
86+
def test_all_subclass_of_exception(self) -> None:
87+
for cls in [BrainusError, AuthenticationError, RateLimitError, QuotaExceededError, APIError]:
88+
assert issubclass(cls, Exception)
89+
90+
def test_can_catch_with_base_class(self) -> None:
91+
try:
92+
raise AuthenticationError("bad key")
93+
except BrainusError as e:
94+
assert e.message == "bad key"
95+
else:
96+
assert False, "Should have caught exception"

0 commit comments

Comments
 (0)