From 450fe0949ec3d04f88e421e1ee32878aa66bb889 Mon Sep 17 00:00:00 2001 From: Tobias Kunze Date: Thu, 4 Dec 2025 17:18:57 +0100 Subject: [PATCH] Fix pytest-coverage setup --- pyproject.toml | 6 +- src/servala/api/views.py | 4 +- src/servala/urls.py | 2 +- src/tests/conftest.py | 70 +++++++++++++++++++++ src/tests/test_api_exoscale.py | 5 +- src/tests/test_compute_plans.py | 22 +++++++ src/tests/test_form_config.py | 108 ++++---------------------------- 7 files changed, 115 insertions(+), 102 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 45be93d..c62bd58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,10 +56,14 @@ extend_exclude = "src/servala/static/mazer" [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "servala.settings_test" -addopts = "-p no:doctest -p no:pastebin -p no:nose --cov=./ --cov-report=term-missing:skip-covered" +addopts = "-p no:doctest -p no:pastebin -p no:nose --cov=./ --cov-report=term-missing:skip-covered --cov-config=pyproject.toml" testpaths = "src/tests" pythonpath = "src" +[tool.coverage.run] +branch = true +omit = ["*/admin.py","*/settings.py","*/wsgi.py", "*/migrations/*", "*/manage.py", "*/__init__.py"] + [tool.bumpver] current_version = "2025.11.17-0" version_pattern = "YYYY.0M.0D-INC0" diff --git a/src/servala/api/views.py b/src/servala/api/views.py index 015e091..9b0082d 100644 --- a/src/servala/api/views.py +++ b/src/servala/api/views.py @@ -239,9 +239,9 @@ The Servala Team""" service_offering = ServiceOffering.objects.get( osb_plan_id=plan_id, service=service ) - except Service.DoesNotExist: # pragma: no-cover + except Service.DoesNotExist: # pragma: no cover return self._error(f"Unknown service_id: {service_id}") - except ServiceOffering.DoesNotExist: # pragma: no-cover + except ServiceOffering.DoesNotExist: # pragma: no cover return self._error( f"Unknown plan_id: {plan_id} for service_id: {service_id}" ) diff --git a/src/servala/urls.py b/src/servala/urls.py index 9eb6705..d932643 100644 --- a/src/servala/urls.py +++ b/src/servala/urls.py @@ -24,7 +24,7 @@ urlpatterns = [ ] # Serve static and media files in development -if settings.DEBUG: +if settings.DEBUG: # pragma: no cover urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += [ diff --git a/src/tests/conftest.py b/src/tests/conftest.py index 09db220..859b4ca 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -4,6 +4,8 @@ import pytest from servala.core.models import ( BillingEntity, + ComputePlan, + ComputePlanAssignment, Organization, OrganizationMembership, OrganizationOrigin, @@ -11,8 +13,11 @@ from servala.core.models import ( ) from servala.core.models.service import ( CloudProvider, + ControlPlane, + ControlPlaneCRD, Service, ServiceCategory, + ServiceDefinition, ServiceOffering, ) @@ -117,3 +122,68 @@ def mock_odoo_failure(mocker): mock_client = mocker.patch("servala.core.models.organization.CLIENT") mock_client.execute.side_effect = Exception("Odoo connection failed") return mock_client + + +@pytest.fixture +def test_control_plane(test_cloud_provider): + return ControlPlane.objects.create( + name="Geneva (CH-GVA-2)", + description="Geneva control plane", + cloud_provider=test_cloud_provider, + api_credentials={ + "server": "https://k8s.example.com", + "token": "test-token", + "certificate_authority_data": "test-ca-data", + }, + ) + + +@pytest.fixture +def test_service_definition(test_service): + return ServiceDefinition.objects.create( + name="Redis Standard", + service=test_service, + api_definition={ + "group": "vshn.appcat.vshn.io", + "version": "v1", + "kind": "VSHNRedis", + }, + ) + + +@pytest.fixture +def test_control_plane_crd( + test_service_offering, test_control_plane, test_service_definition +): + return ControlPlaneCRD.objects.create( + service_offering=test_service_offering, + control_plane=test_control_plane, + service_definition=test_service_definition, + ) + + +@pytest.fixture +def compute_plan(): + return ComputePlan.objects.create( + name="Medium", + description="Medium resource plan", + memory_requests="1Gi", + memory_limits="2Gi", + cpu_requests="500m", + cpu_limits="1000m", + is_active=True, + ) + + +@pytest.fixture +def compute_plan_assignment(compute_plan, test_control_plane_crd): + return ComputePlanAssignment.objects.create( + compute_plan=compute_plan, + control_plane_crd=test_control_plane_crd, + sla="besteffort", + odoo_product_id="test-product-id", + odoo_unit_id="test-unit-id", + price="10.00", + unit="hour", + is_active=True, + ) diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py index 19f8b93..37542e5 100644 --- a/src/tests/test_api_exoscale.py +++ b/src/tests/test_api_exoscale.py @@ -218,11 +218,10 @@ def test_missing_required_fields_error( if isinstance(field_to_remove, tuple): if field_to_remove[0] == "context": del valid_osb_payload["context"][field_to_remove[1]] - elif field_to_remove[0] == "parameters": + else: del valid_osb_payload["parameters"][field_to_remove[1]] else: - if field_to_remove in valid_osb_payload: - del valid_osb_payload[field_to_remove] + del valid_osb_payload[field_to_remove] response = osb_client.put( f"/api/osb/v2/service_instances/{instance_id}", diff --git a/src/tests/test_compute_plans.py b/src/tests/test_compute_plans.py index 2f67b20..283686c 100644 --- a/src/tests/test_compute_plans.py +++ b/src/tests/test_compute_plans.py @@ -295,6 +295,28 @@ def test_build_billing_annotations_no_item_description_without_organization(): assert annotations["servala.com/erp_product_id_storage"] == "storage-product" +@pytest.mark.django_db +def test_compute_plan_assignment_str( + compute_plan_assignment, +): + result = str(compute_plan_assignment) + # Format: "{plan_name} ({sla_display}) → {control_plane_crd}" + assert compute_plan_assignment.compute_plan.name in result + assert "→" in result + + +@pytest.mark.django_db +def test_compute_plan_assignment_get_odoo_reporting_product_id( + compute_plan_assignment, +): + compute_plan_assignment.odoo_product_id = "test-product-xyz" + compute_plan_assignment.save() + + result = compute_plan_assignment.get_odoo_reporting_product_id() + + assert result == "test-product-xyz" + + def test_build_billing_annotations_combined(): compute_plan_assignment = Mock() compute_plan_assignment.odoo_product_id = "compute-product" diff --git a/src/tests/test_form_config.py b/src/tests/test_form_config.py index 25f1bf2..014f1f2 100644 --- a/src/tests/test_form_config.py +++ b/src/tests/test_form_config.py @@ -5,7 +5,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from servala.core.crd import generate_custom_form_class -from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS, MANDATORY_FIELDS +from servala.core.crd.forms import DEFAULT_FIELD_CONFIGS from servala.core.forms import ServiceDefinitionAdminForm from servala.core.models import ControlPlaneCRD @@ -806,10 +806,10 @@ def test_two_element_choices_work_correctly(): errors = [] for field in config_with_proper_choices["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], spec_schema, "spec", errors - ) + assert field["type"] == "choice" + form._validate_choice_field( + field, field["controlplane_field_mapping"], spec_schema, "spec", errors + ) assert len(errors) == 0, f"Expected no errors but got: {errors}" version_field = config_with_proper_choices["fieldsets"][0]["fields"][0] @@ -836,10 +836,10 @@ def test_empty_choices_fail_validation(): errors = [] for field in config_with_empty_choice["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], {}, "spec", errors - ) + assert field["type"] == "choice" + form._validate_choice_field( + field, field["controlplane_field_mapping"], {}, "spec", errors + ) assert len(errors) > 0 assert "must have 1 or 2 elements" in str(errors[0]) @@ -867,10 +867,10 @@ def test_three_plus_element_choices_fail_validation(): errors = [] for field in config_with_long_choice["fieldsets"][0]["fields"]: - if field.get("type") == "choice": - form._validate_choice_field( - field, field["controlplane_field_mapping"], {}, "spec", errors - ) + assert field["type"] == "choice" + form._validate_choice_field( + field, field["controlplane_field_mapping"], {}, "spec", errors + ) assert len(errors) > 0 assert "must have 1 or 2 elements" in str(errors[0]) @@ -936,88 +936,6 @@ def test_field_with_default_config_can_override_defaults(): assert name_field.help_text == DEFAULT_FIELD_CONFIGS["name"]["help_text"] -def test_admin_form_validates_mandatory_fields_present(): - - mock_crd = Mock() - mock_crd.resource_schema = { - "properties": { - "spec": { - "properties": { - "environment": { - "type": "string", - "enum": ["dev", "prod"], - } - } - } - } - } - - config_without_name = { - "fieldsets": [ - { - "fields": [ - { - "type": "choice", - "label": "Environment", - "controlplane_field_mapping": "spec.environment", - "choices": [["dev", "Development"]], - }, - ] - } - ] - } - - errors = [] - included_mappings = set() - for fieldset in config_without_name.get("fieldsets", []): - for field in fieldset.get("fields", []): - mapping = field.get("controlplane_field_mapping") - included_mappings.add(mapping) - - for mandatory_field in MANDATORY_FIELDS: - if mandatory_field not in included_mappings: - errors.append(f"Required field '{mandatory_field}' must be included") - - assert len(errors) > 0 - assert "name" in str(errors[0]).lower() - - -def test_admin_form_validates_fields_without_defaults_need_label_and_type(): - config_with_incomplete_field = { - "fieldsets": [ - { - "fields": [ - {"controlplane_field_mapping": "name"}, # Has defaults - OK - { - "controlplane_field_mapping": "spec.unknown", # No defaults - # Missing label and type - }, - ] - } - ] - } - - errors = [] - - for fieldset in config_with_incomplete_field.get("fieldsets", []): - for field in fieldset.get("fields", []): - mapping = field.get("controlplane_field_mapping") - - if mapping not in DEFAULT_FIELD_CONFIGS: - if not field.get("label"): - errors.append( - f"Field with mapping '{mapping}' must have a 'label' property" - ) - if not field.get("type"): - errors.append( - f"Field with mapping '{mapping}' must have a 'type' property" - ) - - assert len(errors) == 2 - assert any("label" in str(e) for e in errors) - assert any("type" in str(e) for e in errors) - - def test_empty_values_dont_override_default_configs(): class TestModel(models.Model):