2025-12-10 12:36:24 +01:00
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
2025-03-23 00:03:04 +01:00
|
|
|
import pytest
|
|
|
|
|
|
2025-12-10 12:36:24 +01:00
|
|
|
from servala.core.models.service import (
|
|
|
|
|
CloudProvider,
|
|
|
|
|
ControlPlane,
|
|
|
|
|
ControlPlaneCRD,
|
|
|
|
|
ServiceInstance,
|
|
|
|
|
ServiceOffering,
|
|
|
|
|
)
|
2025-03-23 00:03:04 +01:00
|
|
|
|
2025-11-14 17:30:29 +01:00
|
|
|
|
2025-03-24 11:28:52 +01:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
"url,redirect",
|
|
|
|
|
(
|
|
|
|
|
("/", "/accounts/login/?next=/"),
|
|
|
|
|
("/accounts/profile/", "/accounts/login/?next=/accounts/profile/"),
|
|
|
|
|
),
|
|
|
|
|
)
|
2025-03-23 00:03:04 +01:00
|
|
|
def test_root_view_redirects_valid_urls(client, url, redirect):
|
|
|
|
|
response = client.get(url)
|
|
|
|
|
assert response.status_code == 302
|
|
|
|
|
assert response.url == redirect
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_root_view_invalid_urls_404(client):
|
|
|
|
|
response = client.get("/foobarbaz/")
|
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_owner_can_access_dashboard(client, org_owner, organization):
|
|
|
|
|
client.force_login(org_owner)
|
|
|
|
|
response = client.get(organization.urls.base)
|
2025-04-09 16:06:45 +02:00
|
|
|
assert organization.namespace == f"org-{organization.pk}"
|
2025-03-23 00:03:04 +01:00
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert organization.name in response.content.decode()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_user_cannot_see_other_organization(client, org_owner, other_organization):
|
|
|
|
|
client.force_login(org_owner)
|
|
|
|
|
response = client.get(other_organization.urls.base)
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
assert other_organization.name not in response.content.decode()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
2025-03-24 11:28:52 +01:00
|
|
|
def test_organization_linked_in_sidebar(
|
|
|
|
|
client, org_owner, organization, other_organization
|
|
|
|
|
):
|
2025-03-23 00:03:04 +01:00
|
|
|
client.force_login(org_owner)
|
|
|
|
|
response = client.get("/", follow=True)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert organization.name in response.content.decode()
|
|
|
|
|
assert other_organization.name not in response.content.decode()
|
2025-11-06 16:38:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_service_detail_redirects_with_single_offering(
|
2025-12-10 12:48:33 +01:00
|
|
|
client, org_owner, organization, service, service_offering
|
2025-11-06 16:38:13 +01:00
|
|
|
):
|
|
|
|
|
client.force_login(org_owner)
|
2025-12-10 12:48:33 +01:00
|
|
|
url = f"/org/{organization.slug}/services/{service.slug}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
response = client.get(url)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 302
|
2025-12-10 12:48:33 +01:00
|
|
|
expected_url = f"/org/{organization.slug}/services/{service.slug}/offering/{service_offering.pk}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
assert response.url == expected_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_service_detail_shows_multiple_offerings(
|
2025-12-10 12:48:33 +01:00
|
|
|
client, org_owner, organization, service, service_offering
|
2025-11-06 16:38:13 +01:00
|
|
|
):
|
|
|
|
|
second_provider = CloudProvider.objects.create(
|
|
|
|
|
name="AWS", description="Amazon Web Services"
|
|
|
|
|
)
|
|
|
|
|
second_offering = ServiceOffering.objects.create(
|
2025-12-10 12:48:33 +01:00
|
|
|
service=service,
|
2025-11-06 16:38:13 +01:00
|
|
|
provider=second_provider,
|
|
|
|
|
description="Redis on AWS",
|
|
|
|
|
osb_plan_id="test-plan-456",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client.force_login(org_owner)
|
2025-12-10 12:48:33 +01:00
|
|
|
url = f"/org/{organization.slug}/services/{service.slug}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
response = client.get(url)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
content = response.content.decode()
|
|
|
|
|
|
2025-12-10 12:48:33 +01:00
|
|
|
assert service_offering.provider.name in content
|
2025-11-06 16:38:13 +01:00
|
|
|
assert second_offering.provider.name in content
|
|
|
|
|
assert "Create Instance" in content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_service_detail_respects_cloud_provider_restrictions(
|
2025-12-10 12:48:33 +01:00
|
|
|
client, org_owner, organization, service, service_offering
|
2025-11-06 16:38:13 +01:00
|
|
|
):
|
|
|
|
|
second_provider = CloudProvider.objects.create(
|
|
|
|
|
name="AWS", description="Amazon Web Services"
|
|
|
|
|
)
|
|
|
|
|
ServiceOffering.objects.create(
|
2025-12-10 12:48:33 +01:00
|
|
|
service=service,
|
2025-11-06 16:38:13 +01:00
|
|
|
provider=second_provider,
|
|
|
|
|
description="Redis on AWS",
|
|
|
|
|
osb_plan_id="test-plan-456",
|
|
|
|
|
)
|
2025-12-10 12:48:33 +01:00
|
|
|
organization.origin.limit_cloudproviders.add(service_offering.provider)
|
2025-11-06 16:38:13 +01:00
|
|
|
|
|
|
|
|
client.force_login(org_owner)
|
2025-12-10 12:48:33 +01:00
|
|
|
url = f"/org/{organization.slug}/services/{service.slug}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
response = client.get(url)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 302
|
2025-12-10 12:48:33 +01:00
|
|
|
expected_url = f"/org/{organization.slug}/services/{service.slug}/offering/{service_offering.pk}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
assert response.url == expected_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_service_detail_no_redirect_with_restricted_multiple_offerings(
|
2025-12-10 12:48:33 +01:00
|
|
|
client, org_owner, organization, service, service_offering
|
2025-11-06 16:38:13 +01:00
|
|
|
):
|
|
|
|
|
second_provider = CloudProvider.objects.create(
|
|
|
|
|
name="AWS", description="Amazon Web Services"
|
|
|
|
|
)
|
|
|
|
|
second_offering = ServiceOffering.objects.create(
|
2025-12-10 12:48:33 +01:00
|
|
|
service=service,
|
2025-11-06 16:38:13 +01:00
|
|
|
provider=second_provider,
|
|
|
|
|
description="Redis on AWS",
|
|
|
|
|
osb_plan_id="test-plan-456",
|
|
|
|
|
)
|
|
|
|
|
third_provider = CloudProvider.objects.create(
|
|
|
|
|
name="Azure", description="Microsoft Azure"
|
|
|
|
|
)
|
|
|
|
|
third_offering = ServiceOffering.objects.create(
|
2025-12-10 12:48:33 +01:00
|
|
|
service=service,
|
2025-11-06 16:38:13 +01:00
|
|
|
provider=third_provider,
|
|
|
|
|
description="Redis on Azure",
|
|
|
|
|
osb_plan_id="test-plan-789",
|
|
|
|
|
)
|
|
|
|
|
organization.origin.limit_cloudproviders.add(
|
2025-12-10 12:48:33 +01:00
|
|
|
service_offering.provider, second_provider
|
2025-11-06 16:38:13 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
client.force_login(org_owner)
|
2025-12-10 12:48:33 +01:00
|
|
|
url = f"/org/{organization.slug}/services/{service.slug}/"
|
2025-11-06 16:38:13 +01:00
|
|
|
response = client.get(url)
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
content = response.content.decode()
|
2025-12-10 12:48:33 +01:00
|
|
|
assert service_offering.provider.name in content
|
2025-11-06 16:38:13 +01:00
|
|
|
assert second_offering.provider.name in content
|
|
|
|
|
assert third_offering.provider.name not in content
|
2025-12-10 12:36:24 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_service_instance_update_spec_pushes_display_name_annotation(
|
2025-12-10 12:48:33 +01:00
|
|
|
organization, control_plane_crd, org_owner
|
2025-12-10 12:36:24 +01:00
|
|
|
):
|
|
|
|
|
instance = ServiceInstance.objects.create(
|
|
|
|
|
name="test-instance",
|
|
|
|
|
display_name="Original Name",
|
|
|
|
|
organization=organization,
|
2025-12-10 12:48:33 +01:00
|
|
|
context=control_plane_crd,
|
2025-12-10 12:36:24 +01:00
|
|
|
)
|
|
|
|
|
mock_api = MagicMock()
|
|
|
|
|
with (
|
|
|
|
|
patch.object(ControlPlane, "custom_objects_api", mock_api),
|
|
|
|
|
patch.object(ControlPlaneCRD, "kind_plural", "testkinds"),
|
|
|
|
|
):
|
|
|
|
|
instance.display_name = "Updated Name"
|
|
|
|
|
instance.update_spec(spec_data={}, updated_by=org_owner)
|
|
|
|
|
mock_api.patch_namespaced_custom_object.assert_called_once()
|
|
|
|
|
call_kwargs = mock_api.patch_namespaced_custom_object.call_args[1]
|
|
|
|
|
assert "metadata" in call_kwargs["body"]
|
|
|
|
|
annotations = call_kwargs["body"]["metadata"]["annotations"]
|
|
|
|
|
assert annotations["servala.com/displayName"] == "Updated Name"
|
|
|
|
|
|
|
|
|
|
instance.refresh_from_db()
|
|
|
|
|
assert instance.display_name == "Updated Name"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_build_billing_annotations_includes_display_name():
|
|
|
|
|
annotations = ServiceInstance._build_billing_annotations(
|
|
|
|
|
compute_plan_assignment=None,
|
|
|
|
|
control_plane=MagicMock(
|
|
|
|
|
storage_plan_odoo_product_id=None,
|
|
|
|
|
storage_plan_odoo_unit_id=None,
|
|
|
|
|
),
|
|
|
|
|
instance_name="test-instance",
|
|
|
|
|
display_name="My Display Name",
|
|
|
|
|
organization=None,
|
|
|
|
|
service=None,
|
|
|
|
|
)
|
|
|
|
|
assert annotations["servala.com/displayName"] == "My Display Name"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.django_db
|
|
|
|
|
def test_build_billing_annotations_omits_display_name_when_none():
|
|
|
|
|
annotations = ServiceInstance._build_billing_annotations(
|
|
|
|
|
compute_plan_assignment=None,
|
|
|
|
|
control_plane=MagicMock(
|
|
|
|
|
storage_plan_odoo_product_id=None,
|
|
|
|
|
storage_plan_odoo_unit_id=None,
|
|
|
|
|
),
|
|
|
|
|
instance_name="test-instance",
|
|
|
|
|
display_name=None,
|
|
|
|
|
organization=None,
|
|
|
|
|
service=None,
|
|
|
|
|
)
|
|
|
|
|
assert "servala.com/displayName" not in annotations
|