Files
ss-tools/backend/tests/services/test_validation_service_crud.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

427 lines
17 KiB
Python

# #region Test.ValidationTaskService.Crud [C:3] [TYPE Module] [SEMANTICS test,validation,crud,task,run]
# @BRIEF Tests for validation_service.py — CRUD: get/list/delete tasks, trigger runs, run history, run detail.
# @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
import pytest
def make_policy(**overrides):
"""Create a mock ValidationPolicy."""
policy = MagicMock()
policy.id = "pol-1"
policy.name = "Test Task"
policy.description = None
policy.environment_id = "env-1"
policy.is_active = True
policy.dashboard_ids = []
policy.provider_id = "prov-1"
policy.schedule_days = None
policy.window_start = None
policy.window_end = None
policy.notify_owners = None
policy.custom_channels = None
policy.alert_condition = None
policy.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
policy.updated_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
policy.sources = []
policy.screenshot_enabled = True
policy.logs_enabled = True
policy.execute_chart_data = False
policy.llm_batch_size = 1
policy.policy_dashboard_concurrency_limit = 3
policy.prompt_template = None
for k, v in overrides.items():
setattr(policy, k, v)
return policy
def make_valid_query_mock(policies=None):
"""Build a SQLAlchemy query mock chain that works for both filtered and unfiltered paths."""
m = MagicMock()
policies = policies or []
first_result = policies[0] if policies else None
m.configure_mock(**{"count.return_value": len(policies)})
m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies})
# query.filter(...).first() — returns first policy
m.configure_mock(**{"filter.return_value.first.return_value": first_result})
# query.filter(...).count()
m.configure_mock(**{"filter.return_value.count.return_value": len(policies)})
# query.filter(...).order_by(...).first() — used by _policy_to_response for last_run
m.configure_mock(**{"filter.return_value.order_by.return_value.first.return_value": None})
# query.filter(...).order_by(...).offset(...).limit(...).all()
m.configure_mock(**{"filter.return_value.order_by.return_value.offset.return_value.limit.return_value.all.return_value": policies})
return m
class TestGetTask:
"""get_task — find by id."""
def test_not_found_raises(self):
from src.services.validation_service import ValidationTaskService
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
svc = ValidationTaskService(db)
with pytest.raises(ValueError, match="not found"):
svc.get_task("nonexistent")
def test_found(self):
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query = lambda model: make_valid_query_mock() if model.__name__ == "ValidationRun" else make_valid_query_mock([policy])
svc = ValidationTaskService(db)
result = svc.get_task("pol-1")
assert result.id == "pol-1"
class TestListTasks:
"""list_tasks — filtering and pagination."""
def _make_qm(self, policies=None):
"""Build a query mock that works for both filtered and unfiltered paths."""
return make_valid_query_mock(policies)
def test_no_filters(self):
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
svc = ValidationTaskService(db)
total, tasks = svc.list_tasks()
assert total == 1
assert len(tasks) == 1
assert tasks[0].id == "pol-1"
def test_filter_active(self):
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
svc = ValidationTaskService(db)
total, tasks = svc.list_tasks(is_active=True)
assert total == 1
assert len(tasks) == 1
def test_filter_environment_id(self):
"""Cover list_tasks line 398: environment_id filter."""
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
svc = ValidationTaskService(db)
total, tasks = svc.list_tasks(environment_id="env-1")
assert total == 1
def test_filter_search(self):
"""Cover list_tasks line 400: search/ilike filter."""
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query = lambda model: self._make_qm() if model.__name__ == "ValidationRun" else self._make_qm([policy])
svc = ValidationTaskService(db)
total, tasks = svc.list_tasks(search="Test")
assert total == 1
class TestDeleteTask:
"""delete_task — with/without runs."""
def test_not_found_raises(self):
from src.services.validation_service import ValidationTaskService
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
svc = ValidationTaskService(db)
with pytest.raises(ValueError, match="not found"):
svc.delete_task("nonexistent")
def test_delete_without_runs(self):
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
svc = ValidationTaskService(db)
svc.delete_task("pol-1", delete_runs=False)
db.delete.assert_called_once_with(policy)
db.commit.assert_called_once()
def test_delete_with_runs(self):
from src.services.validation_service import ValidationTaskService
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
db.query.return_value.filter.return_value.all.return_value = [("run-1",)]
svc = ValidationTaskService(db)
svc.delete_task("pol-1", delete_runs=True)
db.delete.assert_called_once_with(policy)
db.commit.assert_called_once()
class TestCreateTask:
"""create_task — full CRUD with provider/environment validation."""
def test_creates_policy_with_sources(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskCreate, SourceInput
db = MagicMock()
db.add = MagicMock()
db.flush = MagicMock()
db.commit = MagicMock()
db.refresh = MagicMock()
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
mock_svc = MagicMock()
provider = MagicMock()
provider.is_active = True
provider.is_multimodal = True
mock_svc.get_provider.return_value = provider
mock_cls.return_value = mock_svc
cm = MagicMock()
cm.get_environment.return_value = MagicMock()
svc = ValidationTaskService(db, config_manager=cm)
svc._resolve_sources = MagicMock(return_value=[
MagicMock(type="dashboard_id", value="42",
resolved_dashboard_id="42", status="valid", title=None),
])
# Mock _policy_to_response to avoid ORM serialization issues
svc._policy_to_response = MagicMock(return_value=MagicMock())
payload = MagicMock(spec=ValidationTaskCreate)
payload.name = "New Task"
payload.description = "Test desc"
payload.environment_id = "env-1"
payload.is_active = True
payload.dashboard_ids = ["42"]
payload.provider_id = "prov-1"
payload.schedule_days = ["MON", "WED", "FRI"]
payload.window_start = None
payload.window_end = None
payload.notify_owners = True
payload.custom_channels = None
payload.alert_condition = "FAIL_ONLY"
payload.screenshot_enabled = True
payload.logs_enabled = True
payload.execute_chart_data = False
payload.llm_batch_size = 1
payload.policy_dashboard_concurrency_limit = 3
payload.prompt_template = None
payload.sources = None
result = svc.create_task(payload)
assert db.commit.called
svc._resolve_sources.assert_called_once()
def test_creates_with_dashboard_url_sources(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskCreate
db = MagicMock()
db.add = MagicMock()
db.flush = MagicMock()
db.commit = MagicMock()
db.refresh = MagicMock()
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
mock_svc = MagicMock()
provider = MagicMock()
provider.is_active = True
provider.is_multimodal = False # screenshot_enabled=False
mock_svc.get_provider.return_value = provider
mock_cls.return_value = mock_svc
cm = MagicMock()
cm.get_environment.return_value = MagicMock()
svc = ValidationTaskService(db, config_manager=cm)
svc._resolve_sources = MagicMock(return_value=[
MagicMock(type="dashboard_url", value="http://superset/d/42",
resolved_dashboard_id="42", status="valid", title=None),
])
svc._policy_to_response = MagicMock(return_value=MagicMock())
payload = MagicMock(spec=ValidationTaskCreate)
payload.name = "URL Task"
payload.description = None
payload.environment_id = "env-1"
payload.is_active = True
payload.dashboard_ids = None
payload.provider_id = "prov-1"
payload.schedule_days = []
payload.window_start = "09:00"
payload.window_end = "18:00"
payload.notify_owners = None
payload.custom_channels = [{"type": "SLACK", "target": "#alerts"}]
payload.alert_condition = "ALWAYS"
payload.screenshot_enabled = False
payload.logs_enabled = True
payload.execute_chart_data = True
payload.llm_batch_size = 3
payload.policy_dashboard_concurrency_limit = 5
payload.prompt_template = "Check {{dashboard}}"
payload.sources = None
result = svc.create_task(payload)
assert db.commit.called
class TestUpdateTask:
"""update_task — partial update with re-validation."""
def test_not_found_raises(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = None
svc = ValidationTaskService(db)
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {"name": "Updated"}
with pytest.raises(ValueError, match="not found"):
svc.update_task("nonexistent", payload)
def test_sources_update_with_none_is_skipped(self):
"""Cover _apply_sources_update line 502: sources_data is None."""
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
svc = ValidationTaskService(db, config_manager=MagicMock())
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {"sources": None}
result = svc.update_task("pol-1", payload)
assert db.commit.called
def test_updates_name_only(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
svc = ValidationTaskService(db, config_manager=MagicMock())
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {"name": "Updated Name"}
result = svc.update_task("pol-1", payload)
assert db.commit.called
def test_updates_provider_and_environment(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
cm = MagicMock()
cm.get_environment.return_value = MagicMock()
svc = ValidationTaskService(db, config_manager=cm)
with patch("src.services.validation_service.LLMProviderService") as mock_cls:
mock_svc = MagicMock()
provider = MagicMock()
provider.is_active = True
provider.is_multimodal = True
mock_svc.get_provider.return_value = provider
mock_cls.return_value = mock_svc
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {
"provider_id": "prov-2",
"environment_id": "env-2",
"screenshot_enabled": True,
"window_start": "10:00",
"window_end": "20:00",
}
result = svc.update_task("pol-1", payload)
assert db.commit.called
def test_updates_with_sources(self):
from src.services.validation_service import ValidationTaskService
from src.schemas.validation import ValidationTaskUpdate, SourceInput
policy = make_policy()
db = MagicMock()
db.query.return_value.filter.return_value.first.return_value = policy
svc = ValidationTaskService(db, config_manager=MagicMock())
svc._resolve_sources = MagicMock(return_value=[
MagicMock(type="dashboard_id", value="99",
resolved_dashboard_id="99", status="valid", title="New"),
])
payload = MagicMock(spec=ValidationTaskUpdate)
payload.model_dump.return_value = {
"name": "Updated with sources",
"sources": [SourceInput(type="dashboard_id", value="99")],
}
result = svc.update_task("pol-1", payload)
assert db.commit.called
svc._resolve_sources.assert_called_once()
class TestGetRunHistory:
"""get_run_history — paginated run list for a policy."""
def test_empty(self):
from src.services.validation_service import ValidationTaskService
from src.models.llm import ValidationRun
db = MagicMock()
m = MagicMock()
m.configure_mock(**{"count.return_value": 0})
m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": []})
db.query.return_value.filter.return_value = m
svc = ValidationTaskService(db)
total, runs = svc.get_run_history("pol-1")
assert total == 0
assert runs == []
def test_with_runs(self):
from src.services.validation_service import ValidationTaskService
run = MagicMock()
run.id = "run-1"
run.policy_id = "pol-1"
run.task_id = "task-1"
run.status = "completed"
run.trigger = "manual"
run.started_at = None
run.finished_at = None
run.dashboard_count = 3
run.pass_count = 2
run.warn_count = 1
run.fail_count = 0
run.unknown_count = 0
run.created_at = datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
db = MagicMock()
m = MagicMock()
m.configure_mock(**{"count.return_value": 1})
m.configure_mock(**{"order_by.return_value.offset.return_value.limit.return_value.all.return_value": [run]})
db.query.return_value.filter.return_value = m
svc = ValidationTaskService(db)
total, runs = svc.get_run_history("pol-1")
assert total == 1
assert len(runs) == 1
assert runs[0]["status"] == "completed"
# #endregion Test.ValidationTaskService.Crud