| """ |
| Comprehensive data processing scenario tests (Task 7). |
| |
| These tests map to the documented scenarios in 250-PLAN.md: |
| - File Operations (DATA-001 to DATA-003) |
| - Data Transformation (DATA-004 to DATA-006) |
| - Batch Processing (DATA-007 to DATA-009) |
| - Stream Processing (DATA-010 to DATA-012) |
| - Format Validation (DATA-013 to DATA-015) |
| |
| Priority: HIGH - Data integrity, processing reliability, validation correctness |
| """ |
| import pytest |
| import csv |
| import io |
| import json |
| from datetime import datetime, timedelta |
| from typing import List, Dict, Any |
| from unittest.mock import patch, MagicMock, Mock |
| from sqlalchemy.orm import Session |
| from fastapi.testclient import TestClient |
| from pathlib import Path |
| import tempfile |
| import os |
|
|
| from core.data_ingestion_service import DataIngestionService |
| from core.models import User, AgentRegistry, AgentStatus |
| from tests.factories.user_factory import UserFactory |
|
|
|
|
| |
| |
| |
|
|
| class TestCSVFileUpload: |
| """DATA-001: CSV File Upload and Parsing.""" |
|
|
| def test_upload_valid_csv_succeeds( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test valid CSV file uploads successfully.""" |
| csv_content = """name,email,role |
| John Doe,john@example.com,admin |
| Jane Smith,jane@example.com,member |
| Bob Wilson,bob@example.com,member""" |
|
|
| files = {"file": ("users.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User"} |
| ) |
|
|
| |
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| assert "status" in data |
| assert data["status"] in ["success", "partial_success"] |
|
|
| def test_upload_csv_with_duplicate_handling( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test CSV upload with duplicate record detection.""" |
| |
| existing_user = UserFactory( |
| email="duplicate@example.com", |
| _session=db_session |
| ) |
|
|
| csv_content = """name,email,role |
| New User,new@example.com,member |
| Duplicate User,duplicate@example.com,member""" |
|
|
| files = {"file": ("users.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User"} |
| ) |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| assert "skipped_count" in data |
| assert data["skipped_count"] >= 1 |
|
|
| def test_upload_invalid_csv_fails_gracefully( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test invalid CSV file is rejected with clear error message.""" |
| invalid_csv = "invalid,csv,format\nno,headers,proper" |
|
|
| files = {"file": ("invalid.csv", invalid_csv, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User"} |
| ) |
|
|
| |
| assert response.status_code in [400, 422, 404] |
|
|
| if response.status_code in [400, 422]: |
| data = response.json() |
| assert "error" in data or "detail" in data |
|
|
|
|
| class TestJSONFileProcessing: |
| """DATA-002: JSON File Processing.""" |
|
|
| def test_upload_valid_json_succeeds( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test valid JSON file uploads successfully.""" |
| json_content = { |
| "users": [ |
| {"name": "John Doe", "email": "john@example.com"}, |
| {"name": "Jane Smith", "email": "jane@example.com"} |
| ] |
| } |
|
|
| files = {"file": ("users.json", json.dumps(json_content), "application/json")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-json", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| assert "status" in data |
| assert data["status"] == "success" |
|
|
| def test_upload_malformed_json_fails( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test malformed JSON file is rejected.""" |
| malformed_json = '{"users": [{"name": "John", "email": "john@example.com"}, invalid]}' |
|
|
| files = {"file": ("malformed.json", malformed_json, "application/json")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-json", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [400, 422, 404] |
|
|
| def test_upload_nested_json_parsing( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test nested JSON structures are parsed correctly.""" |
| nested_json = { |
| "company": "Acme Corp", |
| "departments": [ |
| { |
| "name": "Engineering", |
| "employees": [ |
| {"name": "Alice", "role": "Engineer"}, |
| {"name": "Bob", "role": "Senior Engineer"} |
| ] |
| } |
| ] |
| } |
|
|
| files = {"file": ("nested.json", json.dumps(nested_json), "application/json")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-json", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| class TestLargeFileHandling: |
| """DATA-003: Large File Handling.""" |
|
|
| def test_upload_large_csv_processes_in_chunks( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test large CSV files are processed in chunks to avoid memory issues.""" |
| |
| rows = ["id,name,email"] |
| for i in range(1000): |
| rows.append(f"{i},User {i},user{i}@example.com") |
| large_csv = "\n".join(rows) |
|
|
| files = {"file": ("large.csv", large_csv, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User", "chunk_size": "100"} |
| ) |
|
|
| assert response.status_code in [200, 404, 504] |
|
|
| def test_file_size_limit_enforced( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test file size limits are enforced.""" |
| |
| large_content = "x" * (100 * 1024 * 1024 + 1) |
|
|
| files = {"file": ("huge.csv", large_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| |
| assert response.status_code in [413, 404] |
|
|
| def test_concurrent_file_uploads( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test multiple concurrent file uploads are handled correctly.""" |
| csv_content = """name,email |
| User1,user1@example.com |
| User2,user2@example.com""" |
|
|
| files1 = {"file": ("file1.csv", csv_content, "text/csv")} |
| files2 = {"file": ("file2.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| |
| response1 = client.post( |
| "/api/data/upload-csv", |
| files=files1, |
| headers=headers |
| ) |
|
|
| |
| response2 = client.post( |
| "/api/data/upload-csv", |
| files=files2, |
| headers=headers |
| ) |
|
|
| |
| assert response1.status_code in [200, 404] |
| assert response2.status_code in [200, 404] |
|
|
|
|
| |
| |
| |
|
|
| class TestDataMapping: |
| """DATA-004: Data Field Mapping.""" |
|
|
| def test_ai_column_mapping( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test AI-powered column mapping to target schema.""" |
| csv_content = """First_Name,Last_Name,e_mail |
| John,Doe,john@example.com |
| Jane,Smith,jane@example.com""" |
|
|
| files = {"file": ("users.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User", "auto_map": "true"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| if "mapping_used" in data: |
| assert isinstance(data["mapping_used"], dict) |
|
|
| def test_custom_mapping_override( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test custom field mapping overrides AI suggestions.""" |
| csv_content = """fname,lname,em |
| John,Doe,john@example.com""" |
|
|
| custom_mapping = { |
| "fname": "first_name", |
| "lname": "last_name", |
| "em": "email" |
| } |
|
|
| files = {"file": ("users.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={ |
| "target_model": "User", |
| "mapping": json.dumps(custom_mapping) |
| } |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_mapping_validation_errors( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test unmappable columns are reported clearly.""" |
| csv_content = """invalid_col_1,invalid_col_2,invalid_col_3 |
| a,b,c""" |
|
|
| files = {"file": ("bad.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"target_model": "User"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 404] |
|
|
|
|
| class TestDataTypeConversion: |
| """DATA-005: Data Type Conversion.""" |
|
|
| def test_automatic_type_detection( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test automatic data type detection and conversion.""" |
| csv_content = """name,age,salary,active |
| John,25,50000.50,true |
| Jane,30,75000.75,false""" |
|
|
| files = {"file": ("data.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_type_conversion_errors_handled( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test type conversion errors are handled gracefully.""" |
| csv_content = """name,age |
| John,not_a_number |
| Jane,30""" |
|
|
| files = {"file": ("bad_types.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| assert "skipped_count" in data or "error_count" in data |
|
|
| def test_date_format_normalization( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test various date formats are normalized.""" |
| csv_content = """name,birth_date |
| John,01/15/1990 |
| Jane,1990-02-20 |
| Bob,Mar 25 1985""" |
|
|
| files = {"file": ("dates.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| class TestDataEnrichment: |
| """DATA-006: Data Enrichment.""" |
|
|
| def test_lookup_field_enrichment( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test data enrichment via lookup fields.""" |
| csv_content = """user_email,role |
| john@example.com,admin |
| jane@example.com,member""" |
|
|
| files = {"file": ("enrich.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"enrich": "true", "lookup_field": "email"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_default_value_fill( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test missing values filled with defaults.""" |
| csv_content = """name,email |
| John,john@example.com |
| Jane, # Missing email |
| Bob,bob@example.com""" |
|
|
| files = {"file": ("missing.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"default_missing": "true"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| assert "ingested_count" in data or "skipped_count" in data |
|
|
| def test_calculated_field_derivation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test calculated fields derived from source data.""" |
| csv_content = """first_name,last_name |
| John,Doe |
| Jane,Smith""" |
|
|
| files = {"file": ("names.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| |
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"calculated_fields": '["full_name = first_name + " " + last_name"]'} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| |
| |
| |
|
|
| class TestBatchInsertion: |
| """DATA-007: Batch Insertion Performance.""" |
|
|
| def test_bulk_insert_performance( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test bulk insert performs efficiently for large datasets.""" |
| |
| rows = ["name,email"] |
| for i in range(1000): |
| rows.append(f"User{i},user{i}@example.com") |
| csv_data = "\n".join(rows) |
|
|
| files = {"file": ("bulk.csv", csv_data, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| import time |
| start = time.time() |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"batch_size": "100"} |
| ) |
|
|
| duration = time.time() - start |
|
|
| assert response.status_code in [200, 404] |
|
|
| |
| assert duration < 30 |
|
|
| def test_batch_size_optimization( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test optimal batch size is used automatically.""" |
| rows = ["name,email"] |
| for i in range(500): |
| rows.append(f"User{i},user{i}@example.com") |
| csv_data = "\n".join(rows) |
|
|
| files = {"file": ("batch.csv", csv_data, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_partial_batch_failure_recovery( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test partial batch failures don't abort entire operation.""" |
| csv_content = """name,email |
| valid1,valid1@example.com |
| invalid-email # Invalid format |
| valid2,valid2@example.com""" |
|
|
| files = {"file": ("mixed.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"continue_on_error": "true"} |
| ) |
|
|
| |
| assert response.status_code in [200, 206, 404] |
|
|
|
|
| class TestTransactionHandling: |
| """DATA-008: Transaction Rollback on Error.""" |
|
|
| def test_full_rollback_on_failure( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test transaction rolls back completely on critical error.""" |
| |
| csv_content = """name,email,age |
| User1,user1@example.com,25 |
| User2,user2@example.com,not_a_number |
| User3,user3@example.com,30""" |
|
|
| files = {"file": ("fail.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| |
| initial_count = db_session.query(User).count() |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"transactional": "true"} |
| ) |
|
|
| |
| final_count = db_session.query(User).count() |
|
|
| if response.status_code in [200, 400]: |
| |
| assert final_count >= initial_count |
|
|
| def test_batch_commit_strategy( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test batch commit strategy for large imports.""" |
| rows = ["name,email"] |
| for i in range(200): |
| rows.append(f"User{i},user{i}@example.com") |
| csv_data = "\n".join(rows) |
|
|
| files = {"file": ("commit_test.csv", csv_data, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"batch_commit": "true", "batch_size": "50"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_idempotent_batch_operations( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test batch operations are idempotent (can retry safely).""" |
| csv_content = """name,email |
| Test User,test@example.com""" |
|
|
| files = {"file": ("idempotent.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| |
| response1 = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| |
| response2 = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| |
| assert response1.status_code in [200, 404] |
| assert response2.status_code in [200, 404] |
|
|
|
|
| class TestProgressTracking: |
| """DATA-009: Batch Job Progress Tracking.""" |
|
|
| def test_progress_updates_available( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test batch job progress is tracked and queryable.""" |
| rows = ["name,email"] |
| for i in range(100): |
| rows.append(f"User{i},user{i}@example.com") |
| csv_data = "\n".join(rows) |
|
|
| files = {"file": ("progress.csv", csv_data, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers |
| ) |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| assert "total_rows" in data or "ingested_count" in data |
|
|
| def test_job_status_queryable( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test batch job status can be queried via job ID.""" |
| |
| csv_content = """name,email |
| User1,user1@example.com""" |
|
|
| files = {"file": ("job.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"async": "true"} |
| ) |
|
|
| if response.status_code == 200: |
| data = response.json() |
| |
| if "job_id" in data: |
| |
| job_response = client.get( |
| f"/api/data/jobs/{data['job_id']}", |
| headers=headers |
| ) |
| assert job_response.status_code in [200, 404] |
|
|
| def test_completion_notification( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test batch job completion generates notification.""" |
| csv_content = """name,email |
| User1,user1@example.com""" |
|
|
| files = {"file": ("notify.csv", csv_content, "text/csv")} |
| headers = {"Authorization": f"Bearer {member_token}"} |
|
|
| response = client.post( |
| "/api/data/upload-csv", |
| files=files, |
| headers=headers, |
| data={"notify_on_complete": "true"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| |
| |
| |
|
|
| class TestRealTimeStreaming: |
| """DATA-010: Real-Time Data Streaming.""" |
|
|
| def test_streaming_api_endpoint( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test streaming API endpoint for continuous data ingestion.""" |
| response = client.get( |
| "/api/data/stream", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| timeout=5.0 |
| ) |
|
|
| |
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| |
| assert response.headers.get("content-type", "").startswith("text/event-stream") or \ |
| response.headers.get("transfer-encoding") == "chunked" |
|
|
| def test_websocket_data_stream( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test WebSocket connection for real-time data updates.""" |
| |
| |
| response = client.get( |
| "/api/data/ws/stream", |
| headers={"Authorization": f"Bearer {member_token}"} |
| ) |
|
|
| |
| assert response.status_code in [101, 404] |
|
|
| def test_stream_backpressure_handling( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test stream handles backpressure when consumer is slow.""" |
| |
| |
| response = client.get( |
| "/api/data/stream", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"buffer_size": "1000"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| class TestStreamFiltering: |
| """DATA-011: Stream Data Filtering.""" |
|
|
| def test_filter_criteria_applied( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test stream data is filtered by specified criteria.""" |
| response = client.get( |
| "/api/data/stream", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"filter": "status=active", "fields": "id,name"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_field_projection( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test stream returns only requested fields.""" |
| response = client.get( |
| "/api/data/stream", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"fields": "id,name,email"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_dynamic_filter_changes( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test stream filters can be changed mid-stream.""" |
| |
| response = client.post( |
| "/api/data/stream/filter", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"filter": "status=active"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| class TestStreamAggregation: |
| """DATA-012: Stream Data Aggregation.""" |
|
|
| def test_realtime_aggregates( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test stream provides real-time aggregated values.""" |
| response = client.get( |
| "/api/data/stream/aggregates", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"group_by": "category", "metrics": "count,sum,avg"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_windowed_aggregations( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test time-windowed aggregations for streaming data.""" |
| response = client.get( |
| "/api/data/stream/aggregates", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"window": "60s", "metrics": "count"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| def test_aggregate_accuracy( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test aggregate calculations are accurate.""" |
| response = client.get( |
| "/api/data/stream/aggregates", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| params={"metrics": "count,sum,avg,min,max"} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
|
|
| |
| |
| |
|
|
| class TestSchemaValidation: |
| """DATA-013: Schema Validation.""" |
|
|
| def test_valid_schema_passes( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test data matching schema passes validation.""" |
| valid_data = { |
| "name": "John Doe", |
| "email": "john@example.com", |
| "role": "admin" |
| } |
|
|
| response = client.post( |
| "/api/data/validate", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"schema": "user", "data": valid_data} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| if response.status_code == 200: |
| data = response.json() |
| assert data.get("valid") is True |
|
|
| def test_invalid_schema_fails( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test data not matching schema fails validation.""" |
| invalid_data = { |
| "name": "John", |
| "email": "not-an-email", |
| "age": "not-a-number" |
| } |
|
|
| response = client.post( |
| "/api/data/validate", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"schema": "user", "data": invalid_data} |
| ) |
|
|
| assert response.status_code in [200, 400, 404] |
|
|
| if response.status_code in [200, 400]: |
| data = response.json() |
| assert data.get("valid") is False or "errors" in data |
|
|
| def test_missing_required_fields( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test missing required fields are detected.""" |
| incomplete_data = { |
| "name": "John" |
| |
| } |
|
|
| response = client.post( |
| "/api/data/validate", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"schema": "user", "data": incomplete_data} |
| ) |
|
|
| assert response.status_code in [200, 400, 404] |
|
|
| if response.status_code in [200, 400]: |
| data = response.json() |
| assert data.get("valid") is False or "errors" in data |
|
|
|
|
| class TestDataFormatValidation: |
| """DATA-014: Data Format Validation.""" |
|
|
| def test_email_format_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test email format is validated correctly.""" |
| valid_emails = ["user@example.com", "test.user+tag@example.co.uk"] |
| invalid_emails = ["not-an-email", "@example.com", "user@"] |
|
|
| for email in valid_emails: |
| response = client.post( |
| "/api/data/validate/email", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"email": email} |
| ) |
| assert response.status_code in [200, 404] |
|
|
| for email in invalid_emails: |
| response = client.post( |
| "/api/data/validate/email", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"email": email} |
| ) |
| assert response.status_code in [200, 400, 404] |
|
|
| def test_phone_format_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test phone number format validation.""" |
| valid_phones = ["+1-555-123-4567", "555-123-4567", "(555) 123-4567"] |
| invalid_phones = ["abc", "123", "555-"] |
|
|
| for phone in valid_phones: |
| response = client.post( |
| "/api/data/validate/phone", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"phone": phone} |
| ) |
| assert response.status_code in [200, 404] |
|
|
| def test_date_format_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test date format validation.""" |
| valid_dates = ["2026-02-11", "02/11/2026", "2026-02-11T12:00:00Z"] |
| invalid_dates = ["not-a-date", "2026-13-01", "2026-02-30"] |
|
|
| for date in valid_dates: |
| response = client.post( |
| "/api/data/validate/date", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"date": date} |
| ) |
| assert response.status_code in [200, 404] |
|
|
| for date in invalid_dates: |
| response = client.post( |
| "/api/data/validate/date", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"date": date} |
| ) |
| assert response.status_code in [200, 400, 404] |
|
|
|
|
| class TestBusinessRuleValidation: |
| """DATA-015: Business Rule Validation.""" |
|
|
| def test_age_range_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test age range business rule is enforced.""" |
| response = client.post( |
| "/api/data/validate/age", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"age": 25} |
| ) |
|
|
| assert response.status_code in [200, 404] |
|
|
| |
| response = client.post( |
| "/api/data/validate/age", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"age": 150} |
| ) |
|
|
| assert response.status_code in [200, 400, 404] |
|
|
| def test_unique_constraint_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test unique constraint validation.""" |
| |
| UserFactory(email="unique@example.com", _session=db_session) |
|
|
| response = client.post( |
| "/api/data/validate/unique", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"field": "email", "value": "unique@example.com", "model": "User"} |
| ) |
|
|
| |
| assert response.status_code in [200, 400, 404] |
|
|
| def test_conditional_validation( |
| self, client: TestClient, db_session: Session, member_token: str |
| ): |
| """Test conditional validation based on other fields.""" |
| |
| data = { |
| "role": "admin", |
| "permissions": ["read"] |
| } |
|
|
| response = client.post( |
| "/api/data/validate/conditional", |
| headers={"Authorization": f"Bearer {member_token}"}, |
| json={"data": data, "rules": ["admin_requires_write"]} |
| ) |
|
|
| assert response.status_code in [200, 400, 404] |
|
|