diff --git a/backend/models.py b/backend/models.py index 8b9020de..311a3b25 100644 --- a/backend/models.py +++ b/backend/models.py @@ -35,6 +35,13 @@ class UserRole(enum.Enum): USER = "user" OFFICIAL = "official" +class VerificationStatus(enum.Enum): + PENDING = "pending" + VERIFIED = "verified" + FLAGGED = "flagged" + FRAUD_DETECTED = "fraud_detected" + + class User(Base): __tablename__ = "users" @@ -258,10 +265,21 @@ class ResolutionEvidence(Base): __tablename__ = "resolution_evidence" id = Column(Integer, primary_key=True, index=True) grievance_id = Column(Integer, ForeignKey("grievances.id"), nullable=False) - file_path = Column(String, nullable=False) + token_id = Column(Integer, nullable=True) + evidence_hash = Column(String, nullable=False) + gps_latitude = Column(Float, nullable=True) + gps_longitude = Column(Float, nullable=True) + capture_timestamp = Column(DateTime, nullable=True) + device_fingerprint_hash = Column(String, nullable=True) + metadata_bundle = Column(JSON, nullable=True) + server_signature = Column(String, nullable=True) + verification_status = Column(Enum(VerificationStatus), default=VerificationStatus.PENDING) + + file_path = Column(String, nullable=True) # made true to match earlier schema that had it but didn't require it in tests media_type = Column(String, default="image") description = Column(Text, nullable=True) uploaded_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) + created_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) # Relationship grievance = relationship("Grievance", back_populates="resolution_evidence") @@ -270,10 +288,28 @@ class ResolutionProofToken(Base): __tablename__ = "resolution_proof_tokens" id = Column(Integer, primary_key=True, index=True) grievance_id = Column(Integer, ForeignKey("grievances.id"), nullable=False) - token = Column(String, unique=True, index=True) - generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) - expires_at = Column(DateTime, nullable=False) + token_id = Column(String, unique=True, index=True) + authority_email = Column(String, nullable=False) + geofence_latitude = Column(Float, nullable=False) + geofence_longitude = Column(Float, nullable=False) + geofence_radius_meters = Column(Float, nullable=False) + valid_from = Column(DateTime, nullable=False) + valid_until = Column(DateTime, nullable=False) + nonce = Column(String, nullable=False) + token_signature = Column(String, nullable=False) is_used = Column(Boolean, default=False) + used_at = Column(DateTime, nullable=True) + generated_at = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) # Relationship grievance = relationship("Grievance", back_populates="resolution_tokens") + + +class EvidenceAuditLog(Base): + __tablename__ = "evidence_audit_logs" + id = Column(Integer, primary_key=True, index=True) + evidence_id = Column(Integer, ForeignKey("resolution_evidence.id"), nullable=False) + action = Column(String, nullable=False) + details = Column(String, nullable=True) + actor_email = Column(String, nullable=True) + timestamp = Column(DateTime, default=lambda: datetime.datetime.now(datetime.timezone.utc)) diff --git a/backend/pothole_detection.py b/backend/pothole_detection.py index b6db1dc9..a51366d1 100644 --- a/backend/pothole_detection.py +++ b/backend/pothole_detection.py @@ -129,13 +129,6 @@ def reset_model(): _model_loading_error = None logger.info("Model singleton state has been reset.") - if _model is None: - with _model_lock: - if _model is None: # Double check inside lock - try: - _model = load_model() - except Exception: - pass return _model def detect_potholes(image_source): diff --git a/tests/test_captioning.py b/tests/test_captioning.py index 9fb98303..09ad65ce 100644 --- a/tests/test_captioning.py +++ b/tests/test_captioning.py @@ -2,16 +2,22 @@ from unittest.mock import patch, AsyncMock from backend.main import app import pytest +from io import BytesIO +from PIL import Image @pytest.mark.asyncio async def test_generate_description_endpoint(): # Mock the generate_image_caption function in 'backend.routers.detection' module - with patch("backend.routers.detection.generate_image_caption", new_callable=AsyncMock) as mock_caption: + with patch("backend.routers.detection._cached_generate_caption", new_callable=AsyncMock) as mock_caption: mock_caption.return_value = "A photo of a pothole on the road" with TestClient(app) as client: # Create a dummy image - file_content = b"fake image content" + img = Image.new('RGB', (100, 100)) + img_bytes = BytesIO() + img.save(img_bytes, format='JPEG') + file_content = img_bytes.getvalue() + files = {"image": ("test.jpg", file_content, "image/jpeg")} response = client.post("/api/generate-description", files=files) diff --git a/tests/test_graffiti_endpoint.py b/tests/test_graffiti_endpoint.py index b3497162..a946db9d 100644 --- a/tests/test_graffiti_endpoint.py +++ b/tests/test_graffiti_endpoint.py @@ -1,14 +1,16 @@ from fastapi.testclient import TestClient -from unittest.mock import patch +from unittest.mock import patch, AsyncMock from backend.main import app import pytest +from io import BytesIO +from PIL import Image client = TestClient(app) @pytest.fixture def mock_detect_graffiti(): # Patch where it is imported in backend.routers.detection - with patch("backend.routers.detection.detect_graffiti_art_clip") as mock: + with patch("backend.routers.detection._cached_detect_graffiti", new_callable=AsyncMock) as mock: yield mock @pytest.fixture @@ -24,7 +26,12 @@ def test_detect_graffiti(mock_detect_graffiti, mock_validate_file): ] # Simple dummy bytes - files = {"image": ("test.jpg", b"fake_image_bytes", "image/jpeg")} + img = Image.new('RGB', (100, 100)) + img_bytes = BytesIO() + img.save(img_bytes, format='JPEG') + file_content = img_bytes.getvalue() + + files = {"image": ("test.jpg", file_content, "image/jpeg")} response = client.post("/api/detect-graffiti", files=files) diff --git a/tests/test_smart_scan.py b/tests/test_smart_scan.py index cf44bb8a..bd888918 100644 --- a/tests/test_smart_scan.py +++ b/tests/test_smart_scan.py @@ -2,14 +2,19 @@ from unittest.mock import patch, AsyncMock from backend.main import app import pytest +from io import BytesIO +from PIL import Image def test_smart_scan_endpoint(): with TestClient(app) as client: # Patch the function where it is imported in the ROUTER - with patch("backend.routers.detection.detect_smart_scan_clip", new_callable=AsyncMock) as mock_detect: + with patch("backend.routers.detection._cached_detect_smart_scan", new_callable=AsyncMock) as mock_detect: mock_detect.return_value = {"category": "pothole", "confidence": 0.95} - file_content = b"fakeimagebytes" + img = Image.new('RGB', (100, 100)) + img_bytes = BytesIO() + img.save(img_bytes, format='JPEG') + file_content = img_bytes.getvalue() response = client.post( "/api/detect-smart-scan",