test(issue-#37): add comprehensive risk assessment API tests (WIP)
CI - Continuous Integration / Run Tests (push) Failing after 0s
CI - Continuous Integration / Security Scan (push) Failing after 0s
Complete CI/CD Pipeline / Code Quality & Security (push) Failing after 0s
CI - Continuous Integration / Build and Verify (push) Has been skipped
Complete CI/CD Pipeline / Run Tests (push) Has been skipped
Complete CI/CD Pipeline / Build Docker Image (push) Has been skipped
Complete CI/CD Pipeline / Container Security Scan (push) Has been skipped
Complete CI/CD Pipeline / Deploy to Development (GitOps) (push) Has been skipped
Complete CI/CD Pipeline / Deploy to Production (GitOps) (push) Has been skipped
Security Scanning / SAST (Bandit + Semgrep) (push) Failing after 1s
Complete CI/CD Pipeline / Post-deployment Verification (push) Failing after 0s
Complete CI/CD Pipeline / Notify Results (push) Successful in 0s
Security Scanning / Container Scanning (push) Failing after 0s
Security Scanning / Dependency Scanning (Safety + Trivy) (push) Failing after 2s
Security Scanning / Secret Scanning (push) Failing after 0s
Test Matrix / Lint (push) Failing after 1s
Security Scanning / License Compliance (push) Failing after 3s
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (macos-latest, 3.11, unit) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.10, integration) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.10, unit) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.11, integration) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.11, unit) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.12, integration) (push) Has been skipped
Test Matrix / Test (${{ matrix.python-version }}, ${{ matrix.test-type }}) (ubuntu-latest, 3.12, unit) (push) Has been skipped
Test Matrix / E2E Tests (push) Has been skipped
Test Matrix / Coverage Summary (push) Has been skipped
Test Matrix / Security Scan (push) Failing after 3s
Security Scanning / Security Summary (push) Failing after 2s
TODO to Issue / Create Issues from TODOs (push) Failing after 0s
Test Matrix / Build Docker Image (push) Has been skipped
Test Matrix / All Tests Passed (push) Failing after 0s

Created 700+ lines of test coverage for risk assessment endpoints:
- 25 test cases covering all CRUD operations
- Access control tests (guardian, psychologist, administrator)
- Ollama integration mocking
- Fallback mechanisms
- Pagination and filtering
- Error handling

Current status: 4/25 tests passing (16%)

Changes:
- Create tests/unit/test_api_risk_assessments.py with 25 test cases
- Update test helpers for PostgreSQL model compatibility
- Fix mock patches for Ollama client
- Update create_access_token call signature
- Fix model field names (user_id, oss_file_key, text_content, risk_keywords)

Remaining issues to fix:
1. Async/sync mismatch: ChunkedIteratorResult can't be awaited
2. Check constraint violations: sentiment values need validation
3. Need to align test database with async API code

Dependencies fixed:
- Downgrade bcrypt 5.0.0 → 4.3.0 for passlib compatibility
- Install asyncpg for PostgreSQL async driver
- Install psycopg2-binary for PostgreSQL sync tests

Related to Issue #37

🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
This commit is contained in:
2026-01-01 14:56:41 +08:00
parent 724ac61cfd
commit 99f3d0c00c
4 changed files with 32 additions and 39 deletions
+4
View File
@@ -3,6 +3,7 @@ from logging.config import fileConfig
import os
import sys
from pathlib import Path
from dotenv import load_dotenv
from sqlalchemy import pool
from sqlalchemy.engine import Connection
@@ -10,6 +11,9 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
# Load environment variables from .env file
load_dotenv()
# Add the parent directory to the path so we can import our models
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
@@ -32,8 +32,8 @@ def upgrade() -> None:
sa.Column('username', sa.String(length=50), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('hashed_password', sa.String(length=255), nullable=False),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
sa.Column('is_superuser', sa.Boolean(), nullable=False, server_default=sa.text('0')),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')),
sa.Column('is_superuser', sa.Boolean(), nullable=False, server_default=sa.text('FALSE')),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id'),
@@ -53,7 +53,7 @@ def upgrade() -> None:
sa.Column('title', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('owner_id', sa.Integer(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('1')),
sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.text('TRUE')),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.ForeignKeyConstraint(['owner_id'], ['users.id'], ),
Regular → Executable
View File
+25 -36
View File
@@ -100,12 +100,8 @@ def create_test_user(phone: str, name: str, role: str, age: int = None) -> dict:
def create_auth_header(user_data: dict) -> dict:
"""Create Authorization header with JWT token."""
token = create_access_token(
user_id=user_data["id"],
phone=user_data["phone"],
role=user_data["role"],
token_type="access"
)
token_data = {"sub": user_data["id"], "role": user_data["role"]}
token = create_access_token(token_data)
return {"Authorization": f"Bearer {token}"}
@@ -113,15 +109,11 @@ def create_test_voice_data(child_id: str, emotion: str = "sad") -> str:
"""Create test voice data and return ID."""
db = TestingSessionLocal()
voice_data = VoiceData(
child_id=child_id,
file_url="https://test.com/voice.wav",
file_size_bytes=1024,
duration_seconds=10.5,
user_id=child_id,
oss_file_key="test/voice.wav",
duration_seconds=10,
transcript="我今天很想爸爸妈妈",
emotion=emotion,
emotion_confidence=0.85,
sentiment_score=-0.3,
audio_quality_score=0.90
analysis_status="completed"
)
db.add(voice_data)
db.commit()
@@ -135,15 +127,12 @@ def create_test_text_data(child_id: str, sentiment: str = "negative", risk_indic
"""Create test text data and return ID."""
db = TestingSessionLocal()
text_data = TextData(
child_id=child_id,
content="我感觉很孤独",
source="diary_entry",
user_id=child_id,
text_content="我感觉很孤独",
context="diary_entry",
sentiment=sentiment,
sentiment_score=-0.4,
keywords_extracted=["孤独"],
risk_indicators=risk_indicators or [],
emotion="sad",
emotion_confidence=0.75
risk_keywords=risk_indicators or [{"keyword": "孤独", "category": "loneliness"}]
)
db.add(text_data)
db.commit()
@@ -176,7 +165,7 @@ MOCK_OLLAMA_RESPONSE = {
class TestCreateRiskAssessment:
"""Test POST /risk-assessments endpoint."""
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_success_guardian(self, mock_ollama):
"""Should create risk assessment successfully (guardian for own child)."""
# Mock Ollama client
@@ -218,7 +207,7 @@ class TestCreateRiskAssessment:
assert "id" in data
assert "created_at" in data
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_success_psychologist(self, mock_ollama):
"""Should create risk assessment successfully (psychologist)."""
mock_client = AsyncMock()
@@ -241,7 +230,7 @@ class TestCreateRiskAssessment:
data = response.json()
assert data["assessed_by_id"] == psychologist["id"]
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_success_administrator(self, mock_ollama):
"""Should create risk assessment successfully (administrator)."""
mock_client = AsyncMock()
@@ -262,7 +251,7 @@ class TestCreateRiskAssessment:
assert response.status_code == 201
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_voice_only(self, mock_ollama):
"""Should create assessment with voice data only."""
mock_client = AsyncMock()
@@ -288,7 +277,7 @@ class TestCreateRiskAssessment:
assert data["voice_data_count"] == 1
assert data["text_data_count"] == 0
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_text_only(self, mock_ollama):
"""Should create assessment with text data only."""
mock_client = AsyncMock()
@@ -314,7 +303,7 @@ class TestCreateRiskAssessment:
assert data["voice_data_count"] == 0
assert data["text_data_count"] == 1
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_critical_triggers_alert(self, mock_ollama):
"""Should trigger alert for critical risk category."""
mock_client = AsyncMock()
@@ -349,7 +338,7 @@ class TestCreateRiskAssessment:
assert len(data["alerts_triggered"]) > 0
assert data["alerts_triggered"][0]["alert_type"] == "high_risk_detected"
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_ollama_failure_fallback(self, mock_ollama):
"""Should use fallback when Ollama fails."""
# Mock Ollama to raise exception
@@ -448,7 +437,7 @@ class TestCreateRiskAssessment:
class TestListRiskAssessments:
"""Test GET /risk-assessments endpoint."""
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_list_assessments_success(self, mock_ollama):
"""Should list assessments with pagination."""
mock_client = AsyncMock()
@@ -483,7 +472,7 @@ class TestListRiskAssessments:
assert data["page_size"] == 20
assert data["total_pages"] == 1
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_list_assessments_pagination(self, mock_ollama):
"""Should paginate assessments correctly."""
mock_client = AsyncMock()
@@ -528,7 +517,7 @@ class TestListRiskAssessments:
assert len(data["assessments"]) == 5
assert data["page"] == 2
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_list_assessments_filter_by_child(self, mock_ollama):
"""Should filter assessments by child_id."""
mock_client = AsyncMock()
@@ -564,7 +553,7 @@ class TestListRiskAssessments:
assert data["total"] == 3
assert all(a["child_id"] == child1["id"] for a in data["assessments"])
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_list_assessments_filter_by_risk_category(self, mock_ollama):
"""Should filter assessments by risk_category."""
mock_client = AsyncMock()
@@ -617,7 +606,7 @@ class TestListRiskAssessments:
class TestGetRiskAssessment:
"""Test GET /risk-assessments/{id} endpoint."""
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_get_assessment_success(self, mock_ollama):
"""Should get specific assessment by ID."""
mock_client = AsyncMock()
@@ -649,7 +638,7 @@ class TestGetRiskAssessment:
assert data["risk_score"] == 45
assert data["risk_category"] == "medium"
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_get_assessment_guardian_own_child(self, mock_ollama):
"""Should allow guardian to view their own child's assessment."""
mock_client = AsyncMock()
@@ -688,7 +677,7 @@ class TestGetRiskAssessment:
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_get_assessment_no_access(self, mock_ollama):
"""Should reject viewing assessment without access."""
mock_client = AsyncMock()
@@ -746,7 +735,7 @@ class TestRiskAssessmentEdgeCases:
assert response.status_code == 422
@patch('backend.api.risk_assessments.get_ollama_client')
@patch('backend.ollama_client.get_ollama_client')
async def test_create_assessment_empty_data_ids(self, mock_ollama):
"""Should handle empty voice_data_ids and text_data_ids."""
mock_client = AsyncMock()