diff --git a/src/servala/core/models/service.py b/src/servala/core/models/service.py
index 9955668..022b7f1 100644
--- a/src/servala/core/models/service.py
+++ b/src/servala/core/models/service.py
@@ -1,5 +1,4 @@
import copy
-import hashlib
import html
import json
import re
@@ -19,6 +18,10 @@ from encrypted_fields.fields import EncryptedJSONField
from kubernetes import client, config
from kubernetes.client.rest import ApiException
+import hashlib
+
+from django.conf import settings
+
from servala.core import rules as perms
from servala.core.models.mixins import ServalaModelMixin
from servala.core.validators import kubernetes_name_validator
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_create.html b/src/servala/frontend/templates/frontend/organizations/service_instance_create.html
index 3c41ddf..8743abf 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_instance_create.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instance_create.html
@@ -26,8 +26,7 @@
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %}
- {% translate "Create" as create_label %}
- {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form form_submit_label=create_label hide_form_errors=True %}
+ {% include "includes/tabbed_fieldset_form.html" with form=custom_service_form expert_form=service_form hide_form_errors=True %}
{% endif %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
index 2be9f7b..2261477 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instance_update.html
@@ -22,8 +22,7 @@
{% translate "Oops! Something went wrong with the service form generation. Please try again later." %}
{% else %}
- {% translate "Update" as update_label %}
- {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form form_submit_label=update_label hide_form_errors=True %}
+ {% include "includes/tabbed_fieldset_form.html" with form=custom_form expert_form=form hide_form_errors=True %}
{% endif %}
diff --git a/src/servala/frontend/templates/frontend/organizations/service_instances.html b/src/servala/frontend/templates/frontend/organizations/service_instances.html
index f2be736..e29ad88 100644
--- a/src/servala/frontend/templates/frontend/organizations/service_instances.html
+++ b/src/servala/frontend/templates/frontend/organizations/service_instances.html
@@ -36,9 +36,7 @@
{{ instance.display_name }}
|
-
- {{ instance.name }}
- |
+ {{ instance.name }} |
{{ instance.context.service_definition.service.name }} |
{{ instance.context.service_offering.provider.name }} |
{{ instance.context.control_plane.name }} |
diff --git a/src/servala/frontend/views/service.py b/src/servala/frontend/views/service.py
index 5b17b48..8a35def 100644
--- a/src/servala/frontend/views/service.py
+++ b/src/servala/frontend/views/service.py
@@ -604,9 +604,6 @@ class ServiceInstanceUpdateView(
current_spec = dict(self.object.spec) if self.object.spec else {}
spec_data = self._deep_merge(current_spec, spec_data)
- if display_name := form_data.get("display_name"):
- self.object.display_name = display_name
-
compute_plan_assignment = None
if self.plan_form.is_valid():
compute_plan_assignment = self.plan_form.cleaned_data.get(
diff --git a/src/tests/conftest.py b/src/tests/conftest.py
index 298eb42..b9fe003 100644
--- a/src/tests/conftest.py
+++ b/src/tests/conftest.py
@@ -18,7 +18,6 @@ from servala.core.models.service import (
Service,
ServiceCategory,
ServiceDefinition,
- ServiceInstance,
ServiceOffering,
)
@@ -43,6 +42,11 @@ def other_organization(origin):
return Organization.objects.create(name="Test Org Alternate", origin=origin)
+@pytest.fixture
+def user():
+ return User.objects.create(email="testuser@example.org", password="test")
+
+
@pytest.fixture
def org_owner(organization):
owner = User.objects.create(email="owner@example.org", password="example")
@@ -76,7 +80,7 @@ def user():
@pytest.fixture
-def service_category():
+def test_service_category():
return ServiceCategory.objects.create(
name="Databases",
description="Database services",
@@ -84,18 +88,18 @@ def service_category():
@pytest.fixture
-def service(service_category):
+def test_service(test_service_category):
return Service.objects.create(
name="Redis",
slug="redis",
- category=service_category,
+ category=test_service_category,
description="Redis database service",
osb_service_id="test-service-123",
)
@pytest.fixture
-def cloud_provider():
+def test_cloud_provider():
return CloudProvider.objects.create(
name="Exoscale",
description="Exoscale cloud provider",
@@ -103,10 +107,10 @@ def cloud_provider():
@pytest.fixture
-def service_offering(service, cloud_provider):
+def test_service_offering(test_service, test_cloud_provider):
return ServiceOffering.objects.create(
- service=service,
- provider=cloud_provider,
+ service=test_service,
+ provider=test_cloud_provider,
description="Redis on Exoscale",
osb_plan_id="test-plan-123",
)
@@ -149,11 +153,11 @@ def mock_odoo_failure(mocker):
@pytest.fixture
-def control_plane(cloud_provider):
+def test_control_plane(test_cloud_provider):
return ControlPlane.objects.create(
name="Geneva (CH-GVA-2)",
description="Geneva control plane",
- cloud_provider=cloud_provider,
+ cloud_provider=test_cloud_provider,
api_credentials={
"server": "https://k8s.example.com",
"token": "test-token",
@@ -163,10 +167,10 @@ def control_plane(cloud_provider):
@pytest.fixture
-def service_definition(service):
+def test_service_definition(test_service):
return ServiceDefinition.objects.create(
name="Redis Standard",
- service=service,
+ service=test_service,
api_definition={
"group": "vshn.appcat.vshn.io",
"version": "v1",
@@ -176,11 +180,13 @@ def service_definition(service):
@pytest.fixture
-def control_plane_crd(service_offering, control_plane, service_definition):
+def test_control_plane_crd(
+ test_service_offering, test_control_plane, test_service_definition
+):
return ControlPlaneCRD.objects.create(
- service_offering=service_offering,
- control_plane=control_plane,
- service_definition=service_definition,
+ service_offering=test_service_offering,
+ control_plane=test_control_plane,
+ service_definition=test_service_definition,
)
@@ -198,10 +204,10 @@ def compute_plan():
@pytest.fixture
-def compute_plan_assignment(compute_plan, control_plane_crd):
+def compute_plan_assignment(compute_plan, test_control_plane_crd):
return ComputePlanAssignment.objects.create(
compute_plan=compute_plan,
- control_plane_crd=control_plane_crd,
+ control_plane_crd=test_control_plane_crd,
sla="besteffort",
odoo_product_id="test-product-id",
odoo_unit_id="test-unit-id",
@@ -209,30 +215,3 @@ def compute_plan_assignment(compute_plan, control_plane_crd):
unit="hour",
is_active=True,
)
-
-
-@pytest.fixture
-def service_instance(organization, control_plane_crd):
- return ServiceInstance.objects.create(
- name="test-abc12345",
- display_name="My Test Instance",
- organization=organization,
- context=control_plane_crd,
- )
-
-
-@pytest.fixture
-def control_plane_with_storage(cloud_provider):
- return ControlPlane.objects.create(
- name="Storage Zone",
- description="Zone with storage billing",
- cloud_provider=cloud_provider,
- api_credentials={
- "server": "https://k8s.example.com",
- "token": "test-token",
- "certificate-authority-data": "test-ca-data",
- },
- storage_plan_odoo_product_id="storage-product-123",
- storage_plan_odoo_unit_id="storage-unit-456",
- storage_plan_price_per_gib="0.10",
- )
diff --git a/src/tests/test_api_exoscale.py b/src/tests/test_api_exoscale.py
index 4362748..87d54e2 100644
--- a/src/tests/test_api_exoscale.py
+++ b/src/tests/test_api_exoscale.py
@@ -55,14 +55,14 @@ def valid_osb_payload():
def test_successful_onboarding_new_organization(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put(
f"/api/osb/v2/service_instances/{instance_id}",
@@ -107,15 +107,15 @@ def test_successful_onboarding_new_organization(
@pytest.mark.django_db
def test_new_organization_inherits_origin(
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
billing_entity,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
exoscale_origin.billing_entity = billing_entity
exoscale_origin.save()
@@ -137,8 +137,8 @@ def test_new_organization_inherits_origin(
@pytest.mark.django_db
def test_duplicate_organization_returns_existing(
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
@@ -148,10 +148,10 @@ def test_duplicate_organization_returns_existing(
osb_guid="test-org-guid-123",
origin=exoscale_origin,
)
- org.limit_osb_services.add(service)
+ org.limit_osb_services.add(test_service)
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put(
f"/api/osb/v2/service_instances/{instance_id}",
@@ -169,13 +169,13 @@ def test_duplicate_organization_returns_existing(
@pytest.mark.django_db
def test_unauthenticated_osb_api_request_fails(
client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = client.put(
f"/api/osb/v2/service_instances/{instance_id}",
@@ -205,15 +205,15 @@ def test_unauthenticated_osb_api_request_fails(
)
def test_missing_required_fields_error(
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
field_to_remove,
expected_error,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
if isinstance(field_to_remove, tuple):
if field_to_remove[0] == "context":
@@ -251,8 +251,10 @@ def test_invalid_service_id_error(osb_client, valid_osb_payload, instance_id):
@pytest.mark.django_db
-def test_invalid_plan_id_error(osb_client, service, valid_osb_payload, instance_id):
- valid_osb_payload["service_id"] = service.osb_service_id
+def test_invalid_plan_id_error(
+ osb_client, test_service, valid_osb_payload, instance_id
+):
+ valid_osb_payload["service_id"] = test_service.osb_service_id
valid_osb_payload["plan_id"] = 99999
response = osb_client.put(
@@ -264,17 +266,17 @@ def test_invalid_plan_id_error(osb_client, service, valid_osb_payload, instance_
assert response.status_code == 400
response_data = json.loads(response.content)
assert (
- f"Unknown plan_id: 99999 for service_id: {service.osb_service_id}"
+ f"Unknown plan_id: 99999 for service_id: {test_service.osb_service_id}"
in response_data["error"]
)
@pytest.mark.django_db
def test_empty_users_array_error(
- osb_client, service, service_offering, valid_osb_payload, instance_id
+ osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = []
response = osb_client.put(
@@ -290,10 +292,10 @@ def test_empty_users_array_error(
@pytest.mark.django_db
def test_multiple_users_error(
- osb_client, service, service_offering, valid_osb_payload, instance_id
+ osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = [
{"email": "user1@example.com", "full_name": "User One"},
{"email": "user2@example.com", "full_name": "User Two"},
@@ -312,10 +314,10 @@ def test_multiple_users_error(
@pytest.mark.django_db
def test_empty_email_address_error(
- osb_client, service, service_offering, valid_osb_payload, instance_id
+ osb_client, test_service, test_service_offering, valid_osb_payload, instance_id
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"] = [
{"email": "", "full_name": "User With No Email"},
]
@@ -348,14 +350,14 @@ def test_invalid_json_error(osb_client, instance_id):
def test_user_creation_with_name_parsing(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"][0]["full_name"] = "John Doe Smith"
response = osb_client.put(
@@ -374,14 +376,14 @@ def test_user_creation_with_name_parsing(
def test_email_normalization(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
valid_osb_payload["parameters"]["users"][0]["email"] = " TEST@EXAMPLE.COM "
response = osb_client.put(
@@ -399,14 +401,14 @@ def test_email_normalization(
def test_odoo_integration_failure_handling(
mock_odoo_failure,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
valid_osb_payload,
exoscale_origin,
instance_id,
):
- valid_osb_payload["service_id"] = service.osb_service_id
- valid_osb_payload["plan_id"] = service_offering.osb_plan_id
+ valid_osb_payload["service_id"] = test_service.osb_service_id
+ valid_osb_payload["plan_id"] = test_service_offering.osb_plan_id
response = osb_client.put(
f"/api/osb/v2/service_instances/{instance_id}",
@@ -423,14 +425,14 @@ def test_odoo_integration_failure_handling(
def test_organization_creation_with_context_only(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
exoscale_origin,
instance_id,
):
payload = {
- "service_id": service.osb_service_id,
- "plan_id": service_offering.osb_plan_id,
+ "service_id": test_service.osb_service_id,
+ "plan_id": test_service_offering.osb_plan_id,
"context": {
"organization_guid": "fallback-org-guid",
"organization_name": "Fallback Organization",
@@ -460,13 +462,13 @@ def test_organization_creation_with_context_only(
def test_delete_offboarding_success(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
instance_id,
):
response = osb_client.delete(
f"/api/osb/v2/service_instances/{instance_id}"
- f"?service_id={service.osb_service_id}&plan_id={service_offering.osb_plan_id}"
+ f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
)
assert response.status_code == 200
@@ -474,9 +476,9 @@ def test_delete_offboarding_success(
@pytest.mark.django_db
-def test_delete_missing_service_id(osb_client, service_offering, instance_id):
+def test_delete_missing_service_id(osb_client, test_service_offering, instance_id):
response = osb_client.delete(
- f"/api/osb/v2/service_instances/{instance_id}?plan_id={service_offering.osb_plan_id}"
+ f"/api/osb/v2/service_instances/{instance_id}?plan_id={test_service_offering.osb_plan_id}"
)
assert response.status_code == 400
@@ -485,9 +487,9 @@ def test_delete_missing_service_id(osb_client, service_offering, instance_id):
@pytest.mark.django_db
-def test_delete_missing_plan_id(osb_client, service, instance_id):
+def test_delete_missing_plan_id(osb_client, test_service, instance_id):
response = osb_client.delete(
- f"/api/osb/v2/service_instances/{instance_id}?service_id={service.osb_service_id}"
+ f"/api/osb/v2/service_instances/{instance_id}?service_id={test_service.osb_service_id}"
)
assert response.status_code == 400
@@ -507,16 +509,16 @@ def test_delete_invalid_service_id(osb_client, instance_id):
@pytest.mark.django_db
-def test_delete_invalid_plan_id(osb_client, service, instance_id):
+def test_delete_invalid_plan_id(osb_client, test_service, instance_id):
response = osb_client.delete(
f"/api/osb/v2/service_instances/{instance_id}"
- f"?service_id={service.osb_service_id}&plan_id=invalid"
+ f"?service_id={test_service.osb_service_id}&plan_id=invalid"
)
assert response.status_code == 400
response_data = json.loads(response.content)
assert (
- f"Unknown plan_id: invalid for service_id: {service.osb_service_id}"
+ f"Unknown plan_id: invalid for service_id: {test_service.osb_service_id}"
in response_data["error"]
)
@@ -525,13 +527,13 @@ def test_delete_invalid_plan_id(osb_client, service, instance_id):
def test_patch_suspension_success(
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
instance_id,
):
payload = {
- "service_id": service.osb_service_id,
- "plan_id": service_offering.osb_plan_id,
+ "service_id": test_service.osb_service_id,
+ "plan_id": test_service_offering.osb_plan_id,
"parameters": {
"users": [
{
@@ -554,9 +556,9 @@ def test_patch_suspension_success(
@pytest.mark.django_db
-def test_patch_missing_service_id(osb_client, service_offering, instance_id):
+def test_patch_missing_service_id(osb_client, test_service_offering, instance_id):
payload = {
- "plan_id": service_offering.osb_plan_id,
+ "plan_id": test_service_offering.osb_plan_id,
"parameters": {"users": []},
}
@@ -572,9 +574,9 @@ def test_patch_missing_service_id(osb_client, service_offering, instance_id):
@pytest.mark.django_db
-def test_patch_missing_plan_id(osb_client, service, instance_id):
+def test_patch_missing_plan_id(osb_client, test_service, instance_id):
payload = {
- "service_id": service.osb_service_id,
+ "service_id": test_service.osb_service_id,
"parameters": {"users": []},
}
@@ -607,8 +609,8 @@ def test_delete_creates_ticket_with_admin_links(
mocker,
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
instance_id,
):
# Mock the create_helpdesk_ticket function
@@ -616,7 +618,7 @@ def test_delete_creates_ticket_with_admin_links(
response = osb_client.delete(
f"/api/osb/v2/service_instances/{instance_id}"
- f"?service_id={service.osb_service_id}&plan_id={service_offering.osb_plan_id}"
+ f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
)
assert response.status_code == 200
@@ -627,10 +629,10 @@ def test_delete_creates_ticket_with_admin_links(
# Check that the description contains an admin URL
assert "admin/core/serviceoffering" in call_kwargs["description"]
- assert f"/{service_offering.pk}/" in call_kwargs["description"]
+ assert f"/{test_service_offering.pk}/" in call_kwargs["description"]
assert (
call_kwargs["title"]
- == f"Exoscale OSB Offboard - {service.name} - {instance_id}"
+ == f"Exoscale OSB Offboard - {test_service.name} - {instance_id}"
)
@@ -639,8 +641,8 @@ def test_patch_creates_ticket_with_user_admin_links(
mocker,
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
instance_id,
org_owner,
):
@@ -648,8 +650,8 @@ def test_patch_creates_ticket_with_user_admin_links(
mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket")
payload = {
- "service_id": service.osb_service_id,
- "plan_id": service_offering.osb_plan_id,
+ "service_id": test_service.osb_service_id,
+ "plan_id": test_service_offering.osb_plan_id,
"parameters": {
"users": [
{
@@ -678,7 +680,8 @@ def test_patch_creates_ticket_with_user_admin_links(
assert "admin/core/user" in call_kwargs["description"]
assert f"/{org_owner.pk}/" in call_kwargs["description"]
assert (
- call_kwargs["title"] == f"Exoscale OSB Suspend - {service.name} - {instance_id}"
+ call_kwargs["title"]
+ == f"Exoscale OSB Suspend - {test_service.name} - {instance_id}"
)
@@ -687,8 +690,8 @@ def test_ticket_includes_organization_and_instance_when_found(
mocker,
mock_odoo_success,
osb_client,
- service,
- service_offering,
+ test_service,
+ test_service_offering,
organization,
):
# Mock the create_helpdesk_ticket function
@@ -696,12 +699,12 @@ def test_ticket_includes_organization_and_instance_when_found(
service_definition = ServiceDefinition.objects.create(
name="Test Definition",
- service=service,
+ service=test_service,
api_definition={"group": "test.example.com", "version": "v1", "kind": "Test"},
)
control_plane = ControlPlane.objects.create(
name="Test Control Plane",
- cloud_provider=service_offering.provider,
+ cloud_provider=test_service_offering.provider,
api_credentials={
"certificate-authority-data": "test",
"server": "https://test",
@@ -709,7 +712,7 @@ def test_ticket_includes_organization_and_instance_when_found(
},
)
crd = ControlPlaneCRD.objects.create(
- service_offering=service_offering,
+ service_offering=test_service_offering,
control_plane=control_plane,
service_definition=service_definition,
)
@@ -724,7 +727,7 @@ def test_ticket_includes_organization_and_instance_when_found(
response = osb_client.delete(
f"/api/osb/v2/service_instances/{instance_name}"
- f"?service_id={service.osb_service_id}&plan_id={service_offering.osb_plan_id}"
+ f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
)
assert response.status_code == 200
diff --git a/src/tests/test_management_commands.py b/src/tests/test_management_commands.py
index f0f0277..714d29a 100644
--- a/src/tests/test_management_commands.py
+++ b/src/tests/test_management_commands.py
@@ -109,7 +109,7 @@ class TestReencryptFieldsCommand:
assert "Starting re-encryption" in output
assert "Re-encrypted 0 ControlPlane objects" in output
- def test_reencrypt_fields_with_control_plane(self, control_plane):
+ def test_reencrypt_fields_with_control_plane(self, test_control_plane):
out = StringIO()
call_command("reencrypt_fields", stdout=out)
@@ -147,11 +147,11 @@ class TestSyncBillingMetadataCommand:
assert "No control planes found with the specified IDs" in out.getvalue()
- def test_sync_billing_metadata_dry_run_with_control_plane(self, control_plane):
+ def test_sync_billing_metadata_dry_run_with_control_plane(self, test_control_plane):
out = StringIO()
call_command("sync_billing_metadata", "--dry-run", stdout=out)
output = out.getvalue()
assert "DRY RUN" in output
assert "Syncing billing metadata on 1 control plane(s)" in output
- assert control_plane.name in output
+ assert test_control_plane.name in output
diff --git a/src/tests/test_service_models.py b/src/tests/test_service_models.py
deleted file mode 100644
index 794fad2..0000000
--- a/src/tests/test_service_models.py
+++ /dev/null
@@ -1,349 +0,0 @@
-"""Tests for servala.core.models.service module."""
-
-from unittest.mock import MagicMock
-
-import pytest
-from django.core.exceptions import ValidationError
-
-from servala.core.models.service import (
- ControlPlane,
- Service,
- ServiceInstance,
- prune_empty_data,
- validate_api_credentials,
- validate_dict,
-)
-
-pytestmark = pytest.mark.django_db
-
-
-@pytest.fixture
-def service_with_external_links(service_category):
- return Service.objects.create(
- name="PostgreSQL",
- slug="postgresql",
- category=service_category,
- description="PostgreSQL database",
- external_links=[
- {"url": "https://docs.example.com", "title": "Docs", "featured": True},
- {"url": "https://github.com/example", "title": "GitHub", "featured": False},
- {"url": "https://api.example.com", "title": "API", "featured": True},
- ],
- )
-
-
-@pytest.mark.parametrize("data", [None, {}])
-def test_validate_dict_allows_empty_by_default(data):
- validate_dict(data)
-
-
-@pytest.mark.parametrize("data", [None, {}])
-def test_validate_dict_raises_when_empty_not_allowed(data):
- with pytest.raises(ValidationError, match="Data may not be empty"):
- validate_dict(data, allow_empty=False)
-
-
-def test_validate_dict_missing_required_fields_raises():
- with pytest.raises(ValidationError, match="Missing required fields"):
- validate_dict({"field1": "v"}, required_fields={"field1", "field2", "field3"})
-
-
-def test_validate_dict_all_required_fields_present_passes():
- validate_dict({"a": 1, "b": 2, "extra": 3}, required_fields={"a", "b"})
-
-
-@pytest.mark.parametrize("data", [None, {}])
-def test_validate_api_credentials_allows_empty(data):
- validate_api_credentials(data)
-
-
-@pytest.mark.parametrize(
- "input_data,expected",
- [
- ({"a": 1, "b": None, "c": 3}, {"a": 1, "c": 3}),
- ({"a": "hello", "b": "", "c": "world"}, {"a": "hello", "c": "world"}),
- ({"a": [1, 2], "b": [], "c": [3]}, {"a": [1, 2], "c": [3]}),
- (
- {"a": {"nested": 1}, "b": {}, "c": {"x": 2}},
- {"a": {"nested": 1}, "c": {"x": 2}},
- ),
- ({"outer": {"inner": {"empty": None}}}, {}),
- (
- {"false_val": False, "zero": 0, "none": None},
- {"false_val": False, "zero": 0},
- ),
- ("string", "string"),
- (42, 42),
- ],
-)
-def test_prune_empty_data(input_data, expected):
- assert prune_empty_data(input_data) == expected
-
-
-def test_prune_empty_data_nested_dicts():
- data = {"level1": {"level2": {"keep": "value", "remove": None, "empty": ""}}}
- assert prune_empty_data(data) == {"level1": {"level2": {"keep": "value"}}}
-
-
-def test_prune_empty_data_in_lists():
- data = {"items": [{"keep": 1}, {"remove": None}, {"also_keep": 2}]}
- assert prune_empty_data(data) == {"items": [{"keep": 1}, {"also_keep": 2}]}
-
-
-def test_service_featured_links_filters_correctly(service_with_external_links):
- featured = service_with_external_links.featured_links
- assert len(featured) == 2
- assert all(link["featured"] for link in featured)
- assert {link["title"] for link in featured} == {"Docs", "API"}
-
-
-@pytest.mark.parametrize(
- "external_links,expected_count",
- [
- (None, 0),
- ([], 0),
- ([{"url": "https://x.com", "title": "X", "featured": False}], 0),
- ],
-)
-def test_service_featured_links_empty_cases(
- service_category, external_links, expected_count
-):
- svc = Service.objects.create(
- name="Test",
- slug="test-svc",
- category=service_category,
- external_links=external_links,
- )
- assert len(svc.featured_links) == expected_count
-
-
-def test_service_str(service):
- assert str(service) == "Redis"
-
-
-def test_service_category_str(service_category):
- assert str(service_category) == "Databases"
-
-
-def test_cloud_provider_str(cloud_provider):
- assert str(cloud_provider) == "Exoscale"
-
-
-def test_control_plane_str(control_plane):
- assert str(control_plane) == "Geneva (CH-GVA-2)"
-
-
-def test_control_plane_test_connection_no_credentials(cloud_provider):
- plane = ControlPlane.objects.create(
- name="No Creds", cloud_provider=cloud_provider, api_credentials={}
- )
- success, message = plane.test_connection()
- assert success is False
- assert "No API credentials" in str(message)
-
-
-def test_service_definition_str(service_definition):
- assert str(service_definition) == "Redis Standard"
-
-
-def test_service_definition_control_planes(service_definition, control_plane_crd):
- assert control_plane_crd.control_plane in service_definition.control_planes
-
-
-def test_control_plane_crd_str(control_plane_crd):
- result = str(control_plane_crd)
- assert "Redis" in result and "Exoscale" in result and "Geneva" in result
-
-
-@pytest.mark.parametrize(
- "prop,expected",
- [("group", "vshn.appcat.vshn.io"), ("version", "v1"), ("kind", "VSHNRedis")],
-)
-def test_control_plane_crd_api_properties(control_plane_crd, prop, expected):
- assert getattr(control_plane_crd, prop) == expected
-
-
-def test_service_offering_str(service_offering):
- result = str(service_offering)
- assert "Redis" in result and "Exoscale" in result
-
-
-def test_service_offering_control_planes(service_offering, control_plane_crd):
- assert control_plane_crd.control_plane in service_offering.control_planes
-
-
-def test_generate_resource_name_consistent(organization, service):
- name1 = ServiceInstance.generate_resource_name(organization, "My Instance", service)
- name2 = ServiceInstance.generate_resource_name(organization, "My Instance", service)
- assert name1 == name2
-
-
-def test_generate_resource_name_different_inputs(organization, service):
- name_a = ServiceInstance.generate_resource_name(organization, "Instance A", service)
- name_b = ServiceInstance.generate_resource_name(organization, "Instance B", service)
- assert name_a != name_b
-
-
-def test_generate_resource_name_attempt_changes_hash(organization, service):
- name0 = ServiceInstance.generate_resource_name(
- organization, "X", service, attempt=0
- )
- name1 = ServiceInstance.generate_resource_name(
- organization, "X", service, attempt=1
- )
- assert name0 != name1
-
-
-def test_generate_resource_name_format(organization, service, settings):
- name = ServiceInstance.generate_resource_name(organization, "Test", service)
- assert name.startswith(f"{settings.SERVALA_INSTANCE_NAME_PREFIX}-")
- assert len(name.split("-")[-1]) == 8
-
-
-@pytest.mark.parametrize(
- "display_name", ["My Instance", "MY INSTANCE", " My Instance "]
-)
-def test_generate_resource_name_normalizes_display_name(
- organization, service, display_name
-):
- canonical = ServiceInstance.generate_resource_name(
- organization, "my instance", service
- )
- assert (
- ServiceInstance.generate_resource_name(organization, display_name, service)
- == canonical
- )
-
-
-@pytest.mark.parametrize("spec_data", [None, {}])
-def test_prepare_spec_data_handles_empty(spec_data):
- assert ServiceInstance._prepare_spec_data(spec_data) == {}
-
-
-def test_prepare_spec_data_prunes_empty_values():
- assert ServiceInstance._prepare_spec_data({"keep": "v", "rm": None}) == {
- "keep": "v"
- }
-
-
-def test_apply_compute_plan_to_spec(compute_plan_assignment):
- result = ServiceInstance._apply_compute_plan_to_spec({}, compute_plan_assignment)
- assert result["parameters"]["size"]["memory"] == "2Gi"
- assert result["parameters"]["size"]["cpu"] == "1000m"
- assert result["parameters"]["size"]["requests"]["memory"] == "1Gi"
- assert result["parameters"]["size"]["requests"]["cpu"] == "500m"
- assert result["parameters"]["service"]["serviceLevel"] == "besteffort"
-
-
-def test_apply_compute_plan_to_spec_none_assignment():
- spec = {"existing": "value"}
- assert ServiceInstance._apply_compute_plan_to_spec(spec, None) == {
- "existing": "value"
- }
-
-
-def test_apply_compute_plan_preserves_existing(compute_plan_assignment):
- spec = {"parameters": {"custom": "setting", "service": {"other": "config"}}}
- result = ServiceInstance._apply_compute_plan_to_spec(spec, compute_plan_assignment)
- assert result["parameters"]["custom"] == "setting"
- assert result["parameters"]["service"]["other"] == "config"
-
-
-def test_build_billing_annotations_display_name():
- cp = MagicMock(storage_plan_odoo_product_id=None, storage_plan_odoo_unit_id=None)
- annotations = ServiceInstance._build_billing_annotations(
- compute_plan_assignment=None, control_plane=cp, display_name="My Service"
- )
- assert annotations["servala.com/displayName"] == "My Service"
-
-
-def test_build_billing_annotations_no_display_name():
- cp = MagicMock(storage_plan_odoo_product_id=None, storage_plan_odoo_unit_id=None)
- annotations = ServiceInstance._build_billing_annotations(
- compute_plan_assignment=None, control_plane=cp, display_name=None
- )
- assert "servala.com/displayName" not in annotations
-
-
-def test_build_billing_annotations_compute_plan(compute_plan_assignment):
- cp = MagicMock(storage_plan_odoo_product_id=None, storage_plan_odoo_unit_id=None)
- annotations = ServiceInstance._build_billing_annotations(
- compute_plan_assignment=compute_plan_assignment, control_plane=cp
- )
- assert annotations["servala.com/erp_product_id_resource"] == "test-product-id"
- assert annotations["servala.com/erp_unit_id_resource"] == "test-unit-id"
-
-
-def test_build_billing_annotations_storage_plan(control_plane_with_storage):
- annotations = ServiceInstance._build_billing_annotations(
- compute_plan_assignment=None, control_plane=control_plane_with_storage
- )
- assert annotations["servala.com/erp_product_id_storage"] == "storage-product-123"
- assert annotations["servala.com/erp_unit_id_storage"] == "storage-unit-456"
-
-
-@pytest.mark.parametrize(
- "error_msg,expected_has_list,expected_errors",
- [
- ("", False, None),
- (None, False, None),
- ("Something went wrong", False, None),
- ("Error: [single error]", False, None),
- ("Validation failed: [e1, e2, e3]", True, ["e1", "e2", "e3"]),
- ],
-)
-def test_format_kubernetes_error(error_msg, expected_has_list, expected_errors):
- result = ServiceInstance._format_kubernetes_error(error_msg)
- assert result["has_list"] == expected_has_list
- assert result["errors"] == expected_errors
-
-
-def test_format_kubernetes_error_strips_quotes():
- result = ServiceInstance._format_kubernetes_error("Errors: [\"quoted\", 'single']")
- assert "quoted" in result["errors"]
- assert "single" in result["errors"]
-
-
-def test_safe_format_error_non_dict():
- assert ServiceInstance._safe_format_error("plain string") == "plain string"
-
-
-def test_safe_format_error_escapes_html():
- error_data = {"message": "", "has_list": False}
- result = ServiceInstance._safe_format_error(error_data)
- assert "