Subagents delivered tests across all uncovered backend modules: Schemas (100%): agent, auth, health, profile, settings, validation Services (98-100%): auth, profile, health, llm, mapping, resource, security, git, superset_lookup, sql_table_extractor, rbac API routes (new): auth, admin, health, environments, plugins, dashboards (helpers, projection, actions, listing), git (config, deps, env, helpers) Clean Release (100%): DTO, facade, policy_engine, stages, repos, preparation, source_isolation, compliance Git services: base, remote_providers Agent module: app, run, middleware, langgraph_setup Core: trace, cleanup, ws_log_handler, timezone, auth (config/oauth/security), matching Reports: normalizer, report_service, type_profiles Notifications: service, providers Also: - .gitignore: add .coverage, *.cover, coverage-* dirs - src/schemas/auth.py: fix AD group DN regex (comma in CN=...) - Remove co-located src/services/__tests__/ (caused pytest module collision)
480 lines
17 KiB
Python
480 lines
17 KiB
Python
# #region Test.Schemas.Validation [C:2] [TYPE Module] [SEMANTICS test,schema,validation,task,run]
|
|
# @BRIEF Tests for schemas/validation.py — SourceInput, SourceResponse, ValidationTaskCreate,
|
|
# ValidationTaskUpdate, ValidationTaskResponse, ValidationTaskListResponse,
|
|
# TriggerRunResponse, RunResponse, RunListResponse, RecordResponse,
|
|
# RecordListResponse, RunDetailResponse.
|
|
# @RELATION BINDS_TO -> [ValidationSchemas]
|
|
# @TEST_EDGE: missing_field -> optional fields default correctly
|
|
# @TEST_EDGE: invalid_type -> Literal constraints reject bad values
|
|
# @TEST_EDGE: serialization -> round-trip via model_dump/model_validate
|
|
|
|
from datetime import datetime, timezone
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from src.schemas.validation import (
|
|
RecordListResponse,
|
|
RecordResponse,
|
|
RunDetailResponse,
|
|
RunListResponse,
|
|
RunResponse,
|
|
SourceInput,
|
|
SourceResponse,
|
|
TriggerRunResponse,
|
|
ValidationTaskCreate,
|
|
ValidationTaskListResponse,
|
|
ValidationTaskResponse,
|
|
ValidationTaskUpdate,
|
|
)
|
|
|
|
|
|
class TestSourceInput:
|
|
"""SourceInput — Literal type and required value."""
|
|
|
|
def test_dashboard_id(self):
|
|
si = SourceInput(type="dashboard_id", value="42")
|
|
assert si.type == "dashboard_id"
|
|
assert si.value == "42"
|
|
|
|
def test_dashboard_url(self):
|
|
si = SourceInput(type="dashboard_url", value="http://superset/dash/42")
|
|
assert si.type == "dashboard_url"
|
|
|
|
def test_invalid_type_raises(self):
|
|
with pytest.raises(ValidationError):
|
|
SourceInput(type="invalid", value="x")
|
|
|
|
def test_serialize_roundtrip(self):
|
|
si = SourceInput(type="dashboard_id", value="42")
|
|
data = si.model_dump()
|
|
restored = SourceInput.model_validate(data)
|
|
assert restored.type == "dashboard_id"
|
|
assert restored.value == "42"
|
|
|
|
|
|
class TestSourceResponse:
|
|
"""SourceResponse — validation source with metadata and from_attributes."""
|
|
|
|
def test_minimal(self):
|
|
sr = SourceResponse(id="s-1", type="dashboard_id", value="42")
|
|
assert sr.status == "valid"
|
|
assert sr.title is None
|
|
assert sr.resolved_dashboard_id is None
|
|
assert sr.last_error is None
|
|
assert sr.last_checked_at is None
|
|
assert sr.created_at is None
|
|
|
|
def test_with_errors(self):
|
|
sr = SourceResponse(id="s-1", type="dashboard_url", value="http://x", status="error", last_error="Invalid")
|
|
assert sr.status == "error"
|
|
assert sr.last_error == "Invalid"
|
|
|
|
def test_serialize_roundtrip(self):
|
|
sr = SourceResponse(id="s-1", type="dashboard_id", value="42", title="Sales")
|
|
data = sr.model_dump()
|
|
restored = SourceResponse.model_validate(data)
|
|
assert restored.title == "Sales"
|
|
|
|
def test_from_attributes(self):
|
|
class FakeORM:
|
|
id = "s-1"
|
|
type = "dashboard_id"
|
|
value = "42"
|
|
status = "valid"
|
|
title = "Sales"
|
|
resolved_dashboard_id = "42"
|
|
last_error = None
|
|
last_checked_at = None
|
|
created_at = None
|
|
sr = SourceResponse.model_validate(FakeORM())
|
|
assert sr.id == "s-1"
|
|
assert sr.resolved_dashboard_id == "42"
|
|
|
|
|
|
class TestValidationTaskCreate:
|
|
"""ValidationTaskCreate — create task with v1/v2 fields."""
|
|
|
|
def test_minimal(self):
|
|
task = ValidationTaskCreate(
|
|
name="Test Task",
|
|
environment_id="env-1",
|
|
provider_id="prov-1",
|
|
)
|
|
assert task.description is None
|
|
assert task.dashboard_ids == []
|
|
assert task.is_active is True
|
|
assert task.screenshot_enabled is True
|
|
assert task.logs_enabled is True
|
|
assert task.execute_chart_data is False
|
|
assert task.llm_batch_size == 1
|
|
assert task.policy_dashboard_concurrency_limit == 3
|
|
assert task.sources is None
|
|
|
|
def test_with_all_fields(self):
|
|
sources = [SourceInput(type="dashboard_id", value="42")]
|
|
task = ValidationTaskCreate(
|
|
name="Full Task",
|
|
description="Full description",
|
|
environment_id="env-1",
|
|
dashboard_ids=["42", "99"],
|
|
provider_id="prov-1",
|
|
schedule_days=[0, 2, 4],
|
|
window_start="09:00",
|
|
window_end="18:00",
|
|
notify_owners=False,
|
|
custom_channels=["slack:#alerts"],
|
|
alert_condition="WARN_AND_FAIL",
|
|
is_active=False,
|
|
screenshot_enabled=False,
|
|
logs_enabled=False,
|
|
execute_chart_data=True,
|
|
llm_batch_size=5,
|
|
policy_dashboard_concurrency_limit=10,
|
|
prompt_template="Custom template",
|
|
sources=sources,
|
|
)
|
|
assert task.name == "Full Task"
|
|
assert task.schedule_days == [0, 2, 4]
|
|
assert task.execute_chart_data is True
|
|
assert task.llm_batch_size == 5
|
|
assert task.sources == sources
|
|
|
|
def test_llm_batch_size_ge_one(self):
|
|
with pytest.raises(ValidationError):
|
|
ValidationTaskCreate(name="T", environment_id="e1", provider_id="p1", llm_batch_size=0)
|
|
|
|
def test_concurrency_limit_ge_one(self):
|
|
with pytest.raises(ValidationError):
|
|
ValidationTaskCreate(name="T", environment_id="e1", provider_id="p1", policy_dashboard_concurrency_limit=0)
|
|
|
|
def test_serialize_roundtrip(self):
|
|
task = ValidationTaskCreate(name="T", environment_id="e1", provider_id="p1")
|
|
data = task.model_dump()
|
|
restored = ValidationTaskCreate.model_validate(data)
|
|
assert restored.name == "T"
|
|
assert restored.is_active is True
|
|
|
|
|
|
class TestValidationTaskUpdate:
|
|
"""ValidationTaskUpdate — all fields optional for partial update."""
|
|
|
|
def test_empty(self):
|
|
u = ValidationTaskUpdate()
|
|
assert u.name is None
|
|
assert u.description is None
|
|
assert u.environment_id is None
|
|
assert u.dashboard_ids is None
|
|
assert u.provider_id is None
|
|
assert u.schedule_days is None
|
|
assert u.is_active is None
|
|
assert u.sources is None
|
|
|
|
def test_partial(self):
|
|
u = ValidationTaskUpdate(name="Renamed", is_active=False)
|
|
assert u.name == "Renamed"
|
|
assert u.is_active is False
|
|
|
|
def test_serialize_roundtrip(self):
|
|
u = ValidationTaskUpdate(name="New", screenshot_enabled=False)
|
|
data = u.model_dump()
|
|
restored = ValidationTaskUpdate.model_validate(data)
|
|
assert restored.name == "New"
|
|
assert restored.screenshot_enabled is False
|
|
|
|
|
|
class TestValidationTaskResponse:
|
|
"""ValidationTaskResponse — full task response with from_attributes."""
|
|
|
|
def test_minimal(self):
|
|
ts = datetime.now(timezone.utc)
|
|
task = ValidationTaskResponse(
|
|
id="task-1",
|
|
name="My Task",
|
|
environment_id="env-1",
|
|
is_active=True,
|
|
dashboard_ids=["42"],
|
|
created_at=ts,
|
|
)
|
|
assert task.description is None
|
|
assert task.provider_id is None
|
|
assert task.last_run_status is None
|
|
assert task.screenshot_enabled is True
|
|
assert task.llm_batch_size == 1
|
|
assert task.sources == []
|
|
|
|
def test_with_all_fields(self):
|
|
ts = datetime.now(timezone.utc)
|
|
sources = [SourceResponse(id="s-1", type="dashboard_id", value="42")]
|
|
task = ValidationTaskResponse(
|
|
id="task-1", name="Full", environment_id="env-1", is_active=True,
|
|
dashboard_ids=["42"], provider_id="prov-1",
|
|
schedule_days=[1], window_start="09:00", window_end="18:00",
|
|
notify_owners=False, custom_channels=["email"], alert_condition="ALWAYS",
|
|
created_at=ts, updated_at=ts,
|
|
last_run_status="PASS", last_run_at=ts,
|
|
last_run_summary={"pass": 5, "fail": 0},
|
|
screenshot_enabled=False, logs_enabled=True, execute_chart_data=True,
|
|
llm_batch_size=3, policy_dashboard_concurrency_limit=5,
|
|
prompt_template="custom", sources=sources,
|
|
)
|
|
assert task.last_run_status == "PASS"
|
|
assert task.last_run_summary == {"pass": 5, "fail": 0}
|
|
assert len(task.sources) == 1
|
|
|
|
def test_serialize_roundtrip(self):
|
|
ts = datetime.now(timezone.utc)
|
|
task = ValidationTaskResponse(id="t-1", name="T", environment_id="e1", is_active=True, dashboard_ids=[], created_at=ts)
|
|
data = task.model_dump()
|
|
restored = ValidationTaskResponse.model_validate(data)
|
|
assert restored.name == "T"
|
|
|
|
def test_from_attributes(self):
|
|
ts = datetime.now(timezone.utc)
|
|
class FakeORM:
|
|
id = "t-1"
|
|
name = "Task"
|
|
description = "Desc"
|
|
environment_id = "e1"
|
|
is_active = True
|
|
dashboard_ids = ["42"]
|
|
provider_id = "p1"
|
|
schedule_days = [1]
|
|
window_start = "09:00"
|
|
window_end = "18:00"
|
|
notify_owners = True
|
|
custom_channels = None
|
|
alert_condition = "FAIL_ONLY"
|
|
created_at = ts
|
|
updated_at = ts
|
|
last_run_status = "PASS"
|
|
last_run_at = ts
|
|
last_run_summary = None
|
|
screenshot_enabled = True
|
|
logs_enabled = True
|
|
execute_chart_data = False
|
|
llm_batch_size = 1
|
|
policy_dashboard_concurrency_limit = 3
|
|
prompt_template = None
|
|
sources = []
|
|
task = ValidationTaskResponse.model_validate(FakeORM())
|
|
assert task.name == "Task"
|
|
assert task.provider_id == "p1"
|
|
assert task.screenshot_enabled is True
|
|
|
|
|
|
class TestValidationTaskListResponse:
|
|
"""ValidationTaskListResponse — paginated list."""
|
|
|
|
def test_defaults(self):
|
|
resp = ValidationTaskListResponse()
|
|
assert resp.items == []
|
|
assert resp.total == 0
|
|
assert resp.page == 1
|
|
assert resp.page_size == 20
|
|
|
|
def test_with_items(self):
|
|
ts = datetime.now(timezone.utc)
|
|
items = [ValidationTaskResponse(id="t-1", name="T", environment_id="e1", is_active=True, dashboard_ids=[], created_at=ts)]
|
|
resp = ValidationTaskListResponse(items=items, total=1, page=1, page_size=10)
|
|
assert len(resp.items) == 1
|
|
assert resp.total == 1
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = ValidationTaskListResponse(total=5, page=2, page_size=10)
|
|
data = resp.model_dump()
|
|
restored = ValidationTaskListResponse.model_validate(data)
|
|
assert restored.total == 5
|
|
assert restored.page == 2
|
|
|
|
|
|
class TestTriggerRunResponse:
|
|
"""TriggerRunResponse — spawned task response."""
|
|
|
|
def test_minimal(self):
|
|
resp = TriggerRunResponse(task_id="t-1", spawned_task_id="st-1")
|
|
assert resp.run_id is None
|
|
assert resp.status == "PENDING"
|
|
assert resp.message == "Validation task spawned"
|
|
|
|
def test_with_run_id(self):
|
|
resp = TriggerRunResponse(task_id="t-1", spawned_task_id="st-1", run_id="run-1", status="RUNNING")
|
|
assert resp.run_id == "run-1"
|
|
assert resp.status == "RUNNING"
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = TriggerRunResponse(task_id="t-1", spawned_task_id="st-1")
|
|
data = resp.model_dump()
|
|
restored = TriggerRunResponse.model_validate(data)
|
|
assert restored.task_id == "t-1"
|
|
assert restored.spawned_task_id == "st-1"
|
|
|
|
|
|
class TestRunResponse:
|
|
"""RunResponse — v2 ValidationRun display."""
|
|
|
|
def test_minimal(self):
|
|
resp = RunResponse(id="run-1", policy_id="pol-1", status="PENDING")
|
|
assert resp.task_id is None
|
|
assert resp.trigger == "manual"
|
|
assert resp.started_at is None
|
|
assert resp.finished_at is None
|
|
assert resp.dashboard_count == 0
|
|
assert resp.created_at is None
|
|
|
|
def test_with_all_fields(self):
|
|
ts = datetime.now(timezone.utc)
|
|
resp = RunResponse(
|
|
id="run-1", policy_id="pol-1", task_id="t-1", status="RUNNING",
|
|
trigger="scheduled", started_at=ts, finished_at=None,
|
|
dashboard_count=5, pass_count=3, warn_count=1, fail_count=1,
|
|
unknown_count=0, created_at=ts,
|
|
)
|
|
assert resp.trigger == "scheduled"
|
|
assert resp.pass_count == 3
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = RunResponse(id="r-1", policy_id="p-1", status="COMPLETED")
|
|
data = resp.model_dump()
|
|
restored = RunResponse.model_validate(data)
|
|
assert restored.status == "COMPLETED"
|
|
|
|
def test_from_attributes(self):
|
|
class FakeORM:
|
|
id = "r-1"
|
|
policy_id = "p-1"
|
|
task_id = "t-1"
|
|
status = "RUNNING"
|
|
trigger = "manual"
|
|
started_at = None
|
|
finished_at = None
|
|
dashboard_count = 10
|
|
pass_count = 8
|
|
warn_count = 1
|
|
fail_count = 1
|
|
unknown_count = 0
|
|
created_at = None
|
|
resp = RunResponse.model_validate(FakeORM())
|
|
assert resp.dashboard_count == 10
|
|
|
|
|
|
class TestRunListResponse:
|
|
"""RunListResponse — paginated run list."""
|
|
|
|
def test_defaults(self):
|
|
resp = RunListResponse()
|
|
assert resp.items == []
|
|
assert resp.total == 0
|
|
assert resp.page == 1
|
|
assert resp.page_size == 20
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = RunListResponse(total=3, page=1, page_size=10)
|
|
data = resp.model_dump()
|
|
restored = RunListResponse.model_validate(data)
|
|
assert restored.total == 3
|
|
|
|
|
|
class TestRecordResponse:
|
|
"""RecordResponse — validation record with lots of optional fields."""
|
|
|
|
def test_minimal(self):
|
|
ts = datetime.now(timezone.utc)
|
|
resp = RecordResponse(id="rec-1", dashboard_id="42", status="PASS", timestamp=ts)
|
|
assert resp.policy_id is None
|
|
assert resp.run_id is None
|
|
assert resp.dashboard_title is None
|
|
assert resp.summary == ""
|
|
assert resp.issues == []
|
|
assert resp.screenshot_paths == []
|
|
|
|
def test_with_all_fields(self):
|
|
ts = datetime.now(timezone.utc)
|
|
resp = RecordResponse(
|
|
id="rec-1", policy_id="pol-1", run_id="run-1", dashboard_id="42",
|
|
dashboard_title="Sales", environment_id="env-1", status="FAIL",
|
|
summary="Error", timestamp=ts, execution_path="screenshot",
|
|
screenshot_path="/screens/1.png", screenshot_paths=["/screens/1.png"],
|
|
issues=[{"severity": "FAIL", "message": "broken"}],
|
|
raw_response='{"ok": true}', dataset_health={"status": "OK"},
|
|
chart_data_results=[{"chart": "c1"}],
|
|
tab_screenshots=[{"tab": "tab1"}],
|
|
logs_sent_to_llm=["log1"], token_usage={"prompt": 100},
|
|
timings={"total": 5.0},
|
|
)
|
|
assert resp.dashboard_title == "Sales"
|
|
assert len(resp.issues) == 1
|
|
assert resp.timings == {"total": 5.0}
|
|
|
|
def test_serialize_roundtrip(self):
|
|
ts = datetime.now(timezone.utc)
|
|
resp = RecordResponse(id="rec-1", dashboard_id="42", status="PASS", timestamp=ts)
|
|
data = resp.model_dump()
|
|
restored = RecordResponse.model_validate(data)
|
|
assert restored.dashboard_id == "42"
|
|
|
|
def test_from_attributes(self):
|
|
ts = datetime.now(timezone.utc)
|
|
class FakeORM:
|
|
id = "rec-1"
|
|
policy_id = "p-1"
|
|
run_id = "r-1"
|
|
dashboard_id = "42"
|
|
dashboard_title = "Sales"
|
|
environment_id = "e1"
|
|
status = "WARN"
|
|
summary = "Warning"
|
|
timestamp = ts
|
|
execution_path = None
|
|
screenshot_path = None
|
|
screenshot_paths = []
|
|
issues = []
|
|
raw_response = None
|
|
dataset_health = None
|
|
chart_data_results = []
|
|
tab_screenshots = []
|
|
logs_sent_to_llm = []
|
|
token_usage = None
|
|
timings = None
|
|
resp = RecordResponse.model_validate(FakeORM())
|
|
assert resp.dashboard_title == "Sales"
|
|
assert resp.status == "WARN"
|
|
|
|
|
|
class TestRecordListResponse:
|
|
"""RecordListResponse — paginated record list."""
|
|
|
|
def test_defaults(self):
|
|
resp = RecordListResponse()
|
|
assert resp.items == []
|
|
assert resp.total == 0
|
|
assert resp.page == 1
|
|
assert resp.page_size == 20
|
|
|
|
def test_serialize_roundtrip(self):
|
|
resp = RecordListResponse(total=10, page=1, page_size=25)
|
|
data = resp.model_dump()
|
|
restored = RecordListResponse.model_validate(data)
|
|
assert restored.total == 10
|
|
|
|
|
|
class TestRunDetailResponse:
|
|
"""RunDetailResponse — run with records."""
|
|
|
|
def test_minimal(self):
|
|
run = RunResponse(id="r-1", policy_id="p-1", status="COMPLETED")
|
|
resp = RunDetailResponse(run=run)
|
|
assert resp.records == []
|
|
|
|
def test_with_records(self):
|
|
run = RunResponse(id="r-1", policy_id="p-1", status="COMPLETED")
|
|
resp = RunDetailResponse(run=run, records=[{"id": "rec-1"}, {"id": "rec-2"}])
|
|
assert len(resp.records) == 2
|
|
|
|
def test_serialize_roundtrip(self):
|
|
run = RunResponse(id="r-1", policy_id="p-1", status="COMPLETED")
|
|
resp = RunDetailResponse(run=run)
|
|
data = resp.model_dump()
|
|
restored = RunDetailResponse.model_validate(data)
|
|
assert restored.run.id == "r-1"
|
|
# #endregion Test.Schemas.Validation
|