- profile_preference_service: 19 tests, 100% coverage (CRUD, validation, encryption, DTO conversion with mocked AuthRepository and EncryptionManager) - resource_service: 50 tests, 100% coverage (dashboard/dataset enrichment with git/task status, pagination, activity summary, datetime normalization) - validation_service: 63 tests across 3 files, 97% coverage (provider validation, environment validation, source resolution, run/record conversion, trigger_run, create/update/delete tasks, list/filter runs, get_run_detail)
428 lines
17 KiB
Python
428 lines
17 KiB
Python
# #region Test.ValidationTaskService.Helpers [C:3] [TYPE Module] [SEMANTICS test,validation,helpers,utilities]
|
|
# @BRIEF Tests for validation_service.py — helper functions, provider/environment validation, run/record conversion, source resolution, time fields.
|
|
# @RELATION BINDS_TO -> [ValidationService]
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
from datetime import UTC, datetime, time
|
|
import pytest
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""_extract_screenshot_paths, _parse_time, _format_time."""
|
|
|
|
def test_extract_screenshot_paths_none(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
assert _extract_screenshot_paths(None) == []
|
|
|
|
def test_extract_screenshot_paths_valid(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths('{"screenshot_paths": ["/path/1.png", "/path/2.png"]}')
|
|
assert result == ["/path/1.png", "/path/2.png"]
|
|
|
|
def test_extract_screenshot_paths_empty_json(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths('{"other": "data"}')
|
|
assert result == []
|
|
|
|
def test_extract_screenshot_paths_invalid_json(self):
|
|
from src.services.validation_service import _extract_screenshot_paths
|
|
result = _extract_screenshot_paths("not-json")
|
|
assert result == []
|
|
|
|
def test_parse_time_none(self):
|
|
from src.services.validation_service import _parse_time
|
|
assert _parse_time(None) is None
|
|
|
|
def test_parse_time_valid(self):
|
|
from src.services.validation_service import _parse_time
|
|
result = _parse_time("09:30")
|
|
assert result == time(9, 30)
|
|
|
|
def test_parse_time_invalid(self):
|
|
from src.services.validation_service import _parse_time
|
|
assert _parse_time("abc") is None
|
|
|
|
def test_format_time_none(self):
|
|
from src.services.validation_service import _format_time
|
|
assert _format_time(None) is None
|
|
|
|
def test_format_time_valid(self):
|
|
from src.services.validation_service import _format_time
|
|
assert _format_time(time(14, 5)) == "14:05"
|
|
|
|
|
|
class TestValidateProvider:
|
|
"""_validate_provider — existence, active, multimodal checks."""
|
|
|
|
@pytest.fixture
|
|
def service(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
return ValidationTaskService(db, config_manager=None)
|
|
|
|
def test_empty_provider_id_raises(self, service):
|
|
with pytest.raises(ValueError, match="provider_id is required"):
|
|
service._validate_provider("")
|
|
|
|
def test_none_provider_id_raises(self, service):
|
|
with pytest.raises(ValueError, match="provider_id is required"):
|
|
service._validate_provider(" ")
|
|
|
|
def test_not_found_raises(self, service):
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = None
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not found"):
|
|
service._validate_provider("prov-1")
|
|
|
|
def test_inactive_raises(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = False
|
|
provider.name = "GPT-4"
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not active"):
|
|
service._validate_provider("prov-1")
|
|
|
|
def test_non_multimodal_raises(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = False
|
|
provider.name = "GPT-4"
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
with pytest.raises(ValueError, match="not multimodal"):
|
|
service._validate_provider("prov-1", require_multimodal=True)
|
|
|
|
def test_multimodal_not_required(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = False
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
result = service._validate_provider("prov-1", require_multimodal=False)
|
|
assert result is provider
|
|
|
|
def test_valid_multimodal(self, service):
|
|
provider = MagicMock()
|
|
provider.is_active = True
|
|
provider.is_multimodal = True
|
|
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
|
|
mock_svc = MagicMock()
|
|
mock_svc.get_provider.return_value = provider
|
|
mock_cls.return_value = mock_svc
|
|
result = service._validate_provider("prov-1")
|
|
assert result is provider
|
|
|
|
|
|
class TestValidateEnvironment:
|
|
"""_validate_environment — existence check."""
|
|
|
|
def test_no_config_manager_skips(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock(), config_manager=None)
|
|
svc._validate_environment("env-1")
|
|
|
|
def test_environment_found(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = MagicMock()
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
svc._validate_environment("env-1")
|
|
|
|
def test_environment_not_found(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
cm = MagicMock()
|
|
cm.get_environment.return_value = None
|
|
svc = ValidationTaskService(MagicMock(), config_manager=cm)
|
|
with pytest.raises(ValueError, match="not found"):
|
|
svc._validate_environment("env-unknown")
|
|
|
|
|
|
class TestRunToDict:
|
|
"""_run_to_dict — conversion with duration_ms."""
|
|
|
|
def make_run(self, **overrides):
|
|
run = MagicMock()
|
|
run.id = "run-1"
|
|
run.policy_id = "pol-1"
|
|
run.task_id = "task-1"
|
|
run.status = "completed"
|
|
run.trigger = "manual"
|
|
run.dashboard_count = 5
|
|
run.pass_count = 3
|
|
run.warn_count = 1
|
|
run.fail_count = 1
|
|
run.unknown_count = 0
|
|
run.started_at = None
|
|
run.finished_at = None
|
|
run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
for k, v in overrides.items():
|
|
setattr(run, k, v)
|
|
return run
|
|
|
|
def test_no_dates(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(self.make_run())
|
|
assert result["duration_ms"] is None
|
|
assert result["status"] == "completed"
|
|
|
|
def test_with_dates_computes_duration(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
run = self.make_run(
|
|
started_at=datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC),
|
|
finished_at=datetime(2026, 1, 1, 12, 3, 30, tzinfo=UTC),
|
|
)
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(run)
|
|
assert result["duration_ms"] == 210000
|
|
|
|
def test_naive_datetime_utc(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
run = self.make_run(
|
|
started_at=datetime(2026, 1, 1, 12, 0, 0),
|
|
finished_at=datetime(2026, 1, 1, 12, 1, 0),
|
|
)
|
|
result = ValidationTaskService(MagicMock())._run_to_dict(run)
|
|
assert result["duration_ms"] == 60000
|
|
|
|
|
|
class TestRecordToDict:
|
|
"""_record_to_dict — conversion with title resolution."""
|
|
|
|
def make_record(self, **overrides):
|
|
from src.models.llm import ValidationRecord as VR
|
|
record = MagicMock(spec=VR)
|
|
record.id = "rec-1"
|
|
record.run_id = "run-1"
|
|
record.policy_id = "pol-1"
|
|
record.dashboard_id = "42"
|
|
record.environment_id = "env-1"
|
|
record.status = "PASS"
|
|
record.summary = "OK"
|
|
record.issues = []
|
|
record.raw_response = None
|
|
record.screenshot_path = None
|
|
record.screenshot_paths = None
|
|
record.execution_path = None
|
|
record.dataset_health = None
|
|
record.chart_data_results = []
|
|
record.tab_screenshots = []
|
|
record.logs_sent_to_llm = []
|
|
record.token_usage = None
|
|
record.timings = None
|
|
record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
for k, v in overrides.items():
|
|
setattr(record, k, v)
|
|
return record
|
|
|
|
def test_basic(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = None
|
|
svc = ValidationTaskService(db)
|
|
result = svc._record_to_dict(self.make_record())
|
|
assert result["status"] == "PASS"
|
|
assert result["screenshot_paths"] == []
|
|
|
|
def test_with_source_title(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
source = MagicMock()
|
|
source.title = "Sales Dashboard"
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = source
|
|
svc = ValidationTaskService(db)
|
|
result = svc._record_to_dict(self.make_record())
|
|
assert result["dashboard_title"] == "Sales Dashboard"
|
|
|
|
def test_screenshot_paths_fallback(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = None
|
|
svc = ValidationTaskService(db)
|
|
result = svc._record_to_dict(self.make_record(
|
|
raw_response='{"screenshot_paths": ["/path/img.png"]}',
|
|
))
|
|
assert result["screenshot_paths"] == ["/path/img.png"]
|
|
|
|
|
|
class TestResolveSources:
|
|
"""_resolve_sources — source resolution from input."""
|
|
|
|
def test_no_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = MagicMock()
|
|
svc._resolve_dashboard_title = MagicMock(return_value=None)
|
|
result = svc._resolve_sources(None, None, "pol-1", "env-1")
|
|
assert result == []
|
|
|
|
def test_dashboard_id_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._resolve_dashboard_title = MagicMock(return_value="Sales Dashboard")
|
|
result = svc._resolve_sources(None, ["42", "99"], "pol-1", "env-1")
|
|
assert len(result) == 2
|
|
assert result[0].type == "dashboard_id"
|
|
assert result[0].value == "42"
|
|
assert result[0].title == "Sales Dashboard"
|
|
assert result[0].status == "valid"
|
|
|
|
def test_dashboard_url_sources(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = MagicMock(return_value={
|
|
"dashboard_id": "42", "dataset_id": "ds-1",
|
|
"partial_recovery": False, "source_url": "http://superset/d/42",
|
|
})
|
|
svc._resolve_dashboard_title = MagicMock(return_value="Sales Dashboard")
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
|
|
result = svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert len(result) == 1
|
|
assert result[0].type == "dashboard_url"
|
|
assert result[0].resolved_dashboard_id == "42"
|
|
|
|
def test_partial_recovery_status(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._parse_superset_link = MagicMock(return_value={
|
|
"dashboard_id": "42", "partial_recovery": True,
|
|
})
|
|
svc._resolve_dashboard_title = MagicMock(return_value=None)
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_url", value="http://superset/d/42")]
|
|
result = svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert result[0].status == "partial_recovery"
|
|
|
|
def test_dashboard_id_type_source_input(self):
|
|
"""Cover _resolve_sources line 199-200: s.type == 'dashboard_id'."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
svc._resolve_dashboard_title = MagicMock(return_value="Dashboard 42")
|
|
from src.schemas.validation import SourceInput
|
|
sources_input = [SourceInput(type="dashboard_id", value="42")]
|
|
result = svc._resolve_sources(sources_input, None, "pol-1", "env-1")
|
|
assert len(result) == 1
|
|
assert result[0].type == "dashboard_id"
|
|
assert result[0].resolved_dashboard_id == "42"
|
|
assert result[0].status == "valid"
|
|
|
|
def test_policy_to_response_with_sources(self):
|
|
"""Cover _policy_to_response lines 242-243: iterating sources."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
# Create mock policy with sources
|
|
policy_mock = MagicMock()
|
|
policy_mock.id = "pol-1"
|
|
policy_mock.name = "Test"
|
|
policy_mock.description = None
|
|
policy_mock.environment_id = "env-1"
|
|
policy_mock.is_active = True
|
|
policy_mock.dashboard_ids = []
|
|
policy_mock.provider_id = "prov-1"
|
|
policy_mock.schedule_days = []
|
|
policy_mock.window_start = None
|
|
policy_mock.window_end = None
|
|
policy_mock.notify_owners = None
|
|
policy_mock.custom_channels = None
|
|
policy_mock.alert_condition = "FAIL_ONLY"
|
|
policy_mock.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.screenshot_enabled = True
|
|
policy_mock.logs_enabled = True
|
|
policy_mock.execute_chart_data = False
|
|
policy_mock.llm_batch_size = 1
|
|
policy_mock.policy_dashboard_concurrency_limit = 3
|
|
policy_mock.prompt_template = None
|
|
|
|
source = MagicMock()
|
|
source.id = "src-1"
|
|
source.type = "dashboard_id"
|
|
source.value = "42"
|
|
source.status = "valid"
|
|
source.title = "Sales Dashboard"
|
|
source.resolved_dashboard_id = "42"
|
|
source.last_error = None
|
|
source.last_checked_at = None
|
|
source.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
policy_mock.sources = [source]
|
|
|
|
# Mock last_run query
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = None
|
|
|
|
svc.db = db
|
|
result = svc._policy_to_response(policy_mock)
|
|
assert result.id == "pol-1"
|
|
assert len(result.sources) == 1
|
|
|
|
def test_record_to_dict_title_fallback(self):
|
|
"""Cover _record_to_dict lines 346-350: resolve title when source has no title."""
|
|
from src.services.validation_service import ValidationTaskService
|
|
from src.models.llm import ValidationRecord as VR
|
|
|
|
record = MagicMock(spec=VR)
|
|
record.id = "rec-1"
|
|
record.run_id = "run-1"
|
|
record.policy_id = "pol-1"
|
|
record.dashboard_id = "42"
|
|
record.environment_id = "env-1"
|
|
record.status = "PASS"
|
|
record.summary = "OK"
|
|
record.issues = []
|
|
record.raw_response = None
|
|
record.screenshot_path = None
|
|
record.screenshot_paths = None
|
|
record.execution_path = None
|
|
record.dataset_health = None
|
|
record.chart_data_results = []
|
|
record.tab_screenshots = []
|
|
record.logs_sent_to_llm = []
|
|
record.token_usage = None
|
|
record.timings = None
|
|
record.timestamp = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
|
|
# Source exists but without title → triggers fallback
|
|
source = MagicMock()
|
|
source.title = None
|
|
source.resolved_dashboard_id = "42"
|
|
|
|
db = MagicMock()
|
|
db.query.return_value.filter.return_value.first.return_value = source
|
|
|
|
svc = ValidationTaskService(db)
|
|
svc._resolve_dashboard_title = MagicMock(return_value="Resolved Dashboard")
|
|
result = svc._record_to_dict(record)
|
|
assert result["dashboard_title"] == "Resolved Dashboard"
|
|
svc._resolve_dashboard_title.assert_called_once_with("env-1", "42")
|
|
|
|
|
|
class TestApplyTimeField:
|
|
"""_apply_time_field — parse and set time fields."""
|
|
|
|
def test_field_not_in_update(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
policy = MagicMock()
|
|
svc._apply_time_field({}, policy, "window_start")
|
|
|
|
def test_field_in_update(self):
|
|
from src.services.validation_service import ValidationTaskService
|
|
svc = ValidationTaskService(MagicMock())
|
|
policy = MagicMock()
|
|
svc._apply_time_field({"window_start": "10:00"}, policy, "window_start")
|
|
assert policy.window_start == time(10, 0)
|
|
# #endregion Test.ValidationTaskService.Helpers
|