Files
ss-tools/backend/tests/services/test_validation_service_helpers.py
busya 442596a3dc test(services): add unit tests for profile_preference, resource, validation_service
- 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)
2026-06-10 14:57:11 +03:00

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