Compare commits
5 commits
3d62595663
...
234a169128
| Author | SHA1 | Date | |
|---|---|---|---|
| 234a169128 | |||
| 3e17e03da9 | |||
| 208f3c357d | |||
| e4c64c4a17 | |||
| 61f1065bc6 |
5 changed files with 487 additions and 17 deletions
|
|
@ -6,6 +6,7 @@ from django.contrib.auth.decorators import login_not_required
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
@ -19,7 +20,8 @@ from servala.core.models import (
|
||||||
OrganizationRole,
|
OrganizationRole,
|
||||||
User,
|
User,
|
||||||
)
|
)
|
||||||
from servala.core.models.service import Service, ServiceOffering
|
from servala.core.models.service import Service, ServiceInstance, ServiceOffering
|
||||||
|
from servala.core.odoo import create_helpdesk_ticket
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -28,9 +30,7 @@ logger = logging.getLogger(__name__)
|
||||||
@method_decorator(login_not_required, name="dispatch")
|
@method_decorator(login_not_required, name="dispatch")
|
||||||
class OSBServiceInstanceView(OSBBasicAuthPermission, View):
|
class OSBServiceInstanceView(OSBBasicAuthPermission, View):
|
||||||
"""
|
"""
|
||||||
OSB API endpoint for service instance provisioning (onboarding).
|
OSB API endpoint for service instance management via Exoscale.
|
||||||
Implements the PUT /v2/service_instances/:instance_id endpoint.
|
|
||||||
https://docs.servala.com/exoscale-osb.html#_onboarding
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _error(self, error):
|
def _error(self, error):
|
||||||
|
|
@ -177,3 +177,169 @@ The Servala Team"""
|
||||||
recipient_list=[user.email],
|
recipient_list=[user.email],
|
||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete(self, request, instance_id):
|
||||||
|
"""
|
||||||
|
This implements the Exoscale offboarding flow MVP.
|
||||||
|
https://docs.servala.com/exoscale-osb.html#_offboarding
|
||||||
|
"""
|
||||||
|
service_id = request.GET.get("service_id")
|
||||||
|
plan_id = request.GET.get("plan_id")
|
||||||
|
|
||||||
|
if not service_id:
|
||||||
|
return self._error("service_id is required but missing.")
|
||||||
|
if not plan_id:
|
||||||
|
return self._error("plan_id is required but missing.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
service = Service.objects.get(osb_service_id=service_id)
|
||||||
|
service_offering = ServiceOffering.objects.get(
|
||||||
|
osb_plan_id=plan_id, service=service
|
||||||
|
)
|
||||||
|
except Service.DoesNotExist:
|
||||||
|
return self._error(f"Unknown service_id: {service_id}")
|
||||||
|
except ServiceOffering.DoesNotExist:
|
||||||
|
return self._error(
|
||||||
|
f"Unknown plan_id: {plan_id} for service_id: {service_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._create_action_helpdesk_ticket(
|
||||||
|
request=request,
|
||||||
|
action="Offboard",
|
||||||
|
instance_id=instance_id,
|
||||||
|
service=service,
|
||||||
|
service_offering=service_offering,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({}, status=200)
|
||||||
|
|
||||||
|
def patch(self, request, instance_id):
|
||||||
|
"""
|
||||||
|
This implements the Exoscale suspension flow MVP.
|
||||||
|
https://docs.servala.com/exoscale-osb.html#_suspension
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = json.loads(request.body)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return JsonResponse({"error": "Invalid JSON in request body"}, status=400)
|
||||||
|
|
||||||
|
service_id = data.get("service_id")
|
||||||
|
plan_id = data.get("plan_id")
|
||||||
|
|
||||||
|
if not service_id:
|
||||||
|
return self._error("service_id is required but missing.")
|
||||||
|
if not plan_id:
|
||||||
|
return self._error("plan_id is required but missing.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
service = Service.objects.get(osb_service_id=service_id)
|
||||||
|
# Special handling: when plan_id is "suspend", don't lookup service_offering
|
||||||
|
service_offering = None
|
||||||
|
if plan_id != "suspend":
|
||||||
|
service_offering = ServiceOffering.objects.get(
|
||||||
|
osb_plan_id=plan_id, service=service
|
||||||
|
)
|
||||||
|
except Service.DoesNotExist:
|
||||||
|
return self._error(f"Unknown service_id: {service_id}")
|
||||||
|
except ServiceOffering.DoesNotExist:
|
||||||
|
return self._error(
|
||||||
|
f"Unknown plan_id: {plan_id} for service_id: {service_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._create_action_helpdesk_ticket(
|
||||||
|
request=request,
|
||||||
|
action="Suspend",
|
||||||
|
instance_id=instance_id,
|
||||||
|
service=service,
|
||||||
|
service_offering=service_offering,
|
||||||
|
users=data.get("parameters", {}).get("users"),
|
||||||
|
)
|
||||||
|
return JsonResponse({}, status=200)
|
||||||
|
|
||||||
|
def _get_admin_url(self, model_name, pk):
|
||||||
|
admin_path = reverse(f"admin:{model_name}", args=[pk])
|
||||||
|
return self.request.build_absolute_uri(admin_path)
|
||||||
|
|
||||||
|
def _create_action_helpdesk_ticket(
|
||||||
|
self, request, action, instance_id, service, service_offering=None, users=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Create an Odoo helpdesk ticket for offboarding or suspension actions.
|
||||||
|
This is an MVP implementation that creates a ticket for manual handling.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
service_instance = None
|
||||||
|
organization = None
|
||||||
|
try:
|
||||||
|
# Look for instances with this name in the service offering's context
|
||||||
|
filter_kwargs = {"name": instance_id}
|
||||||
|
if service_offering:
|
||||||
|
filter_kwargs["context__service_offering"] = service_offering
|
||||||
|
|
||||||
|
service_instance = (
|
||||||
|
ServiceInstance.objects.filter(**filter_kwargs)
|
||||||
|
.select_related("organization")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if service_instance:
|
||||||
|
organization = service_instance.organization
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
description_parts = [f"Action: {action}", f"Service: {service.name}"]
|
||||||
|
if organization:
|
||||||
|
org_url = self._get_admin_url(
|
||||||
|
"core_organization_change", organization.pk
|
||||||
|
)
|
||||||
|
description_parts.append(
|
||||||
|
f"Organization: {organization.name} - {org_url}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if service_instance:
|
||||||
|
instance_url = self._get_admin_url(
|
||||||
|
"core_serviceinstance_change", service_instance.pk
|
||||||
|
)
|
||||||
|
description_parts.append(
|
||||||
|
f"Instance: {service_instance.name} - {instance_url}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
description_parts.append(f"Instance: {instance_id}")
|
||||||
|
|
||||||
|
if service_offering:
|
||||||
|
offering_url = self._get_admin_url(
|
||||||
|
"core_serviceoffering_change", service_offering.pk
|
||||||
|
)
|
||||||
|
description_parts.append(f"Service Offering: {offering_url}")
|
||||||
|
|
||||||
|
if users:
|
||||||
|
description_parts.append("<br/>Users:")
|
||||||
|
for user_data in users:
|
||||||
|
email = user_data.get("email", "N/A")
|
||||||
|
full_name = user_data.get("full_name", "N/A")
|
||||||
|
role = user_data.get("role", "N/A")
|
||||||
|
|
||||||
|
user_link = email
|
||||||
|
if email and email != "N/A":
|
||||||
|
try:
|
||||||
|
user = User.objects.get(email=email.strip().lower())
|
||||||
|
user_link = self._get_admin_url("core_user_change", user.pk)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
|
description_parts.append(f" - {full_name} ({user_link}) - {role}")
|
||||||
|
|
||||||
|
description = "<br/>".join(description_parts)
|
||||||
|
|
||||||
|
create_helpdesk_ticket(
|
||||||
|
title=f"Exoscale OSB {action} - {service.name} - {instance_id}",
|
||||||
|
description=description,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Created {action} helpdesk ticket for instance {instance_id}, service {service.name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(
|
||||||
|
f"Error creating Exoscale {action} helpdesk ticket for instance {instance_id}: {e}"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -207,3 +207,19 @@ def get_invoice_addresses(user):
|
||||||
return invoice_addresses or []
|
return invoice_addresses or []
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def create_helpdesk_ticket(title, description, partner_id=None, sale_order_id=None):
|
||||||
|
ticket_data = {
|
||||||
|
"name": title,
|
||||||
|
"team_id": settings.ODOO["HELPDESK_TEAM_ID"],
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if partner_id:
|
||||||
|
ticket_data["partner_id"] = partner_id
|
||||||
|
|
||||||
|
if sale_order_id:
|
||||||
|
ticket_data["sale_order_id"] = sale_order_id
|
||||||
|
|
||||||
|
return CLIENT.execute("helpdesk.ticket", "create", [ticket_data])
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
|
|
||||||
from servala.core.odoo import CLIENT
|
from servala.core.odoo import create_helpdesk_ticket
|
||||||
from servala.frontend.forms.support import SupportForm
|
from servala.frontend.forms.support import SupportForm
|
||||||
from servala.frontend.views.mixins import OrganizationViewMixin
|
from servala.frontend.views.mixins import OrganizationViewMixin
|
||||||
|
|
||||||
|
|
@ -24,21 +23,16 @@ class SupportView(OrganizationViewMixin, FormView):
|
||||||
if not partner_id:
|
if not partner_id:
|
||||||
raise Exception("Could not get or create Odoo contact for user")
|
raise Exception("Could not get or create Odoo contact for user")
|
||||||
|
|
||||||
ticket_data = {
|
|
||||||
"name": f"Servala Support - Organization {organization.name}",
|
|
||||||
"team_id": settings.ODOO["HELPDESK_TEAM_ID"],
|
|
||||||
"partner_id": partner_id,
|
|
||||||
"description": message,
|
|
||||||
}
|
|
||||||
|
|
||||||
# All orgs should have a sale order ID, but legacy ones might not have it.
|
# All orgs should have a sale order ID, but legacy ones might not have it.
|
||||||
# Also, we want to be very sure that support requests work, especially for
|
# Also, we want to be very sure that support requests work, especially for
|
||||||
# organizations where something in the creation process may have gone wrong,
|
# organizations where something in the creation process may have gone wrong,
|
||||||
# so if the ID does not exist, we omit it entirely.
|
# so if the ID does not exist, we omit it entirely.
|
||||||
if organization.odoo_sale_order_id:
|
create_helpdesk_ticket(
|
||||||
ticket_data["sale_order_id"] = organization.odoo_sale_order_id
|
title=f"Servala Support - Organization {organization.name}",
|
||||||
|
description=message,
|
||||||
CLIENT.execute("helpdesk.ticket", "create", [ticket_data])
|
partner_id=partner_id,
|
||||||
|
sale_order_id=organization.odoo_sale_order_id or None,
|
||||||
|
)
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ overrides/adds settings specific to testing.
|
||||||
from servala.settings import * # noqa: F403, F401
|
from servala.settings import * # noqa: F403, F401
|
||||||
|
|
||||||
SECRET_KEY = "test-secret-key-for-testing-only-do-not-use-in-production"
|
SECRET_KEY = "test-secret-key-for-testing-only-do-not-use-in-production"
|
||||||
|
SALT_KEY = SECRET_KEY
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
"django.contrib.auth.hashers.MD5PasswordHasher",
|
"django.contrib.auth.hashers.MD5PasswordHasher",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@ from django.core import mail
|
||||||
from django_scopes import scopes_disabled
|
from django_scopes import scopes_disabled
|
||||||
|
|
||||||
from servala.core.models import Organization, OrganizationOrigin, User
|
from servala.core.models import Organization, OrganizationOrigin, User
|
||||||
|
from servala.core.models.service import (
|
||||||
|
ControlPlane,
|
||||||
|
ControlPlaneCRD,
|
||||||
|
ServiceDefinition,
|
||||||
|
ServiceInstance,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -451,3 +457,290 @@ def test_organization_creation_with_context_only(
|
||||||
assert response.status_code == 201
|
assert response.status_code == 201
|
||||||
org = Organization.objects.get(osb_guid="fallback-org-guid")
|
org = Organization.objects.get(osb_guid="fallback-org-guid")
|
||||||
assert org is not None
|
assert org is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_offboarding_success(
|
||||||
|
mock_odoo_success,
|
||||||
|
osb_client,
|
||||||
|
test_service,
|
||||||
|
test_service_offering,
|
||||||
|
instance_id,
|
||||||
|
):
|
||||||
|
response = osb_client.delete(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}"
|
||||||
|
f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"{}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
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={test_service_offering.osb_plan_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "service_id is required but missing" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
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={test_service.osb_service_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "plan_id is required but missing" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_invalid_service_id(osb_client, instance_id):
|
||||||
|
response = osb_client.delete(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}?service_id=invalid&plan_id=invalid"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "Unknown service_id: invalid" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
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={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: {test_service.osb_service_id}"
|
||||||
|
in response_data["error"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_patch_suspension_success(
|
||||||
|
mock_odoo_success,
|
||||||
|
osb_client,
|
||||||
|
test_service,
|
||||||
|
test_service_offering,
|
||||||
|
instance_id,
|
||||||
|
):
|
||||||
|
payload = {
|
||||||
|
"service_id": test_service.osb_service_id,
|
||||||
|
"plan_id": test_service_offering.osb_plan_id,
|
||||||
|
"parameters": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"full_name": "Test User",
|
||||||
|
"role": "owner",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = osb_client.patch(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}",
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == b"{}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_patch_missing_service_id(osb_client, test_service_offering, instance_id):
|
||||||
|
payload = {
|
||||||
|
"plan_id": test_service_offering.osb_plan_id,
|
||||||
|
"parameters": {"users": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = osb_client.patch(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}",
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "service_id is required but missing" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_patch_missing_plan_id(osb_client, test_service, instance_id):
|
||||||
|
payload = {
|
||||||
|
"service_id": test_service.osb_service_id,
|
||||||
|
"parameters": {"users": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = osb_client.patch(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}",
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "plan_id is required but missing" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_patch_invalid_json(osb_client, instance_id):
|
||||||
|
response = osb_client.patch(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}",
|
||||||
|
data="invalid json{",
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 400
|
||||||
|
response_data = json.loads(response.content)
|
||||||
|
assert "Invalid JSON in request body" in response_data["error"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_delete_creates_ticket_with_admin_links(
|
||||||
|
mocker,
|
||||||
|
mock_odoo_success,
|
||||||
|
osb_client,
|
||||||
|
test_service,
|
||||||
|
test_service_offering,
|
||||||
|
instance_id,
|
||||||
|
):
|
||||||
|
# Mock the create_helpdesk_ticket function
|
||||||
|
mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket")
|
||||||
|
|
||||||
|
response = osb_client.delete(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}"
|
||||||
|
f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify the ticket was created with admin URL
|
||||||
|
mock_create_ticket.assert_called_once()
|
||||||
|
call_kwargs = mock_create_ticket.call_args[1]
|
||||||
|
|
||||||
|
# Check that the description contains an admin URL
|
||||||
|
assert "admin/core/serviceoffering" in call_kwargs["description"]
|
||||||
|
assert f"/{test_service_offering.pk}/" in call_kwargs["description"]
|
||||||
|
assert (
|
||||||
|
call_kwargs["title"]
|
||||||
|
== f"Exoscale OSB Offboard - {test_service.name} - {instance_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_patch_creates_ticket_with_user_admin_links(
|
||||||
|
mocker,
|
||||||
|
mock_odoo_success,
|
||||||
|
osb_client,
|
||||||
|
test_service,
|
||||||
|
test_service_offering,
|
||||||
|
instance_id,
|
||||||
|
org_owner,
|
||||||
|
):
|
||||||
|
# Mock the create_helpdesk_ticket function
|
||||||
|
mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket")
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"service_id": test_service.osb_service_id,
|
||||||
|
"plan_id": test_service_offering.osb_plan_id,
|
||||||
|
"parameters": {
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"email": org_owner.email,
|
||||||
|
"full_name": "Test User",
|
||||||
|
"role": "owner",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
response = osb_client.patch(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_id}",
|
||||||
|
data=json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify the ticket was created with admin URLs
|
||||||
|
mock_create_ticket.assert_called_once()
|
||||||
|
call_kwargs = mock_create_ticket.call_args[1]
|
||||||
|
|
||||||
|
# Check that the description contains admin URLs
|
||||||
|
assert "admin/core/serviceoffering" in call_kwargs["description"]
|
||||||
|
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 - {test_service.name} - {instance_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_ticket_includes_organization_and_instance_when_found(
|
||||||
|
mocker,
|
||||||
|
mock_odoo_success,
|
||||||
|
osb_client,
|
||||||
|
test_service,
|
||||||
|
test_service_offering,
|
||||||
|
organization,
|
||||||
|
):
|
||||||
|
# Mock the create_helpdesk_ticket function
|
||||||
|
mock_create_ticket = mocker.patch("servala.api.views.create_helpdesk_ticket")
|
||||||
|
|
||||||
|
service_definition = ServiceDefinition.objects.create(
|
||||||
|
name="Test Definition",
|
||||||
|
service=test_service,
|
||||||
|
api_definition={"group": "test.example.com", "version": "v1", "kind": "Test"},
|
||||||
|
)
|
||||||
|
control_plane = ControlPlane.objects.create(
|
||||||
|
name="Test Control Plane",
|
||||||
|
cloud_provider=test_service_offering.provider,
|
||||||
|
api_credentials={
|
||||||
|
"certificate-authority-data": "test",
|
||||||
|
"server": "https://test",
|
||||||
|
"token": "test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
crd = ControlPlaneCRD.objects.create(
|
||||||
|
service_offering=test_service_offering,
|
||||||
|
control_plane=control_plane,
|
||||||
|
service_definition=service_definition,
|
||||||
|
)
|
||||||
|
instance_name = "test-instance-123"
|
||||||
|
service_instance = ServiceInstance.objects.create(
|
||||||
|
name=instance_name,
|
||||||
|
organization=organization,
|
||||||
|
context=crd,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = osb_client.delete(
|
||||||
|
f"/api/osb/v2/service_instances/{instance_name}"
|
||||||
|
f"?service_id={test_service.osb_service_id}&plan_id={test_service_offering.osb_plan_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify the ticket was created with all admin URLs
|
||||||
|
mock_create_ticket.assert_called_once()
|
||||||
|
call_kwargs = mock_create_ticket.call_args[1]
|
||||||
|
|
||||||
|
# Check organization is included
|
||||||
|
assert f"Organization: {organization.name}" in call_kwargs["description"]
|
||||||
|
assert "admin/core/organization" in call_kwargs["description"]
|
||||||
|
assert f"/{organization.pk}/" in call_kwargs["description"]
|
||||||
|
|
||||||
|
# Check instance is included
|
||||||
|
assert f"Instance: {service_instance.name}" in call_kwargs["description"]
|
||||||
|
assert "admin/core/serviceinstance" in call_kwargs["description"]
|
||||||
|
assert f"/{service_instance.pk}/" in call_kwargs["description"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue