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
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:
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user