1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-09 03:36:48 +00:00
webvirtcloud/storages/tests/test_views.py
catborise 0610c23a34 fix(api): an error's status says what it is, and its body is always {"detail"}
- libvirt errors all answered 400. A VM, pool or volume the host does not
  have now answers 404, a volume in use 409 (vrtManager.util.InUse) and a
  host that cannot be reached 503, which a client may retry.
- A template's power on or cycle and a stopped pool answered
  {"status": ...} with 409; they answer {"detail": ...}, as every other
  refusal. The schema documents these 409s (ErrorSerializer).
- The volume list and a volume of a stopped pool answered 200 with an
  empty list or a made-up volume; they answer 409.
- An API URL that names nothing answered the HTML error page; it answers
  a JSON 404 after the API's authentication.
- A session that must change its password was redirected to the HTML
  form; an API request of it gets the JSON 403 a token gets.
2026-11-07 13:29:46 +03:00

143 lines
7.1 KiB
Python

from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.contrib.messages import get_messages
from django.test import RequestFactory, TestCase
from django.urls import reverse
from computes.models import Compute
from operations import claims
User = get_user_model()
class GetVolumesViewTests(TestCase):
"""get_volumes must not answer 200 with an empty list when the pool cannot
be read: the caller could not tell that apart from a pool with no volumes."""
def setUp(self):
self.admin = get_user_model().objects.create_superuser(
username="vols_admin", password="password", email="vols_admin@example.com"
)
self.client.login(request=RequestFactory().get("/"), username="vols_admin", password="password")
self.compute = Compute.objects.create(name="VolCompute", hostname="localhost", type=1, login="u", password="p")
self.url = reverse("volumes", args=[self.compute.id, "default"])
@patch("storages.views.wvmStorage")
def test_listing_failure_is_not_a_200(self, mock_wvm):
from libvirt import libvirtError
mock_wvm.return_value.get_volumes.side_effect = libvirtError("pool is gone")
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
self.assertIn("error", response.json())
@patch("storages.views.wvmStorage")
def test_listing_success_returns_the_volumes(self, mock_wvm):
mock_wvm.return_value.get_volumes.return_value = ["b.qcow2", "a.qcow2"]
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["vols"], ["a.qcow2", "b.qcow2"])
class PoolPageTests(TestCase):
def setUp(self):
get_user_model().objects.create_superuser(username="pool_admin", password="password", email="pool_admin@example.com")
self.client.login(request=RequestFactory().get("/"), username="pool_admin", password="password")
self.compute = Compute.objects.create(name="PoolCompute", hostname="localhost", type=1, login="u", password="p")
@patch("storages.views.wvmStorage")
def test_a_pool_the_host_does_not_have_goes_back_to_the_pools_with_the_reason(self, mock_wvm):
from libvirt import libvirtError
mock_wvm.side_effect = libvirtError("Storage pool not found: no storage pool with matching name 'default'")
response = self.client.get(reverse("storage", args=[self.compute.id, "default"]))
self.assertRedirects(response, reverse("storages", args=[self.compute.id]), fetch_redirect_response=False)
self.assertIn("no storage pool with matching name", str(list(get_messages(response.wsgi_request))[0]))
class VolumeDeleteTests(TestCase):
"""A volume a VM still needs is refused with its reason, on the page and in the API."""
def setUp(self):
User.objects.create_superuser(username="vol_admin", password="password", email="vol_admin@example.com")
self.client.login(request=RequestFactory().get("/"), username="vol_admin", password="password")
self.compute = Compute.objects.create(name="VolCompute", hostname="localhost", type=1, login="u", password="p")
def refused(self, mock_wvm):
from vrtManager import util
conn = mock_wvm.return_value
conn.is_active.return_value = False
conn.get_size.return_value = (1, 1)
conn.del_volume.side_effect = util.InUse("Volume disk.qcow2 is in use")
return conn
@patch("storages.views.wvmStorage")
def test_the_page_shows_the_refusal(self, mock_wvm):
conn = self.refused(mock_wvm)
url = reverse("storage", args=[self.compute.id, "default"])
response = self.client.post(url, {"del_volume": "1", "volname": "disk.qcow2"})
self.assertRedirects(response, url, fetch_redirect_response=False)
conn.del_volume.assert_called_once_with("disk.qcow2")
messages = [str(m) for m in get_messages(response.wsgi_request)]
self.assertTrue(any("disk.qcow2 is in use" in m for m in messages), messages)
self.assertFalse(any("is deleted" in m for m in messages), messages)
def test_the_check_and_the_delete_hold_the_compute_lock(self):
"""No VM operation (they share the compute lock) attaches the volume
between the check and the delete."""
from contextlib import contextmanager
held = []
@contextmanager
def lock(compute, *args, **kwargs):
held.append((compute.pk, kwargs.get("shared", False)))
yield
held.pop()
for module, request in (
("storages.views", lambda: self.client.post(reverse("storage", args=[self.compute.id, "default"]),
{"del_volume": "1", "volname": "disk.qcow2"})),
("storages.api.viewsets", lambda: self.client.delete(
f"/api/v1/computes/{self.compute.id}/storages/default/volumes/disk.qcow2/")),
):
with self.subTest(module), patch(f"{module}.wvmStorage") as mock_wvm, \
patch(f"{module}.libvirt_compute_lock", lock):
conn = mock_wvm.return_value
conn.is_active.return_value = True
conn.get_size.return_value = (1, 1)
inside = []
conn.del_volume.side_effect = lambda name: inside.append(list(held))
request()
self.assertEqual(inside, [[(self.compute.pk, False)]])
@patch("storages.views.wvmStorage")
def test_a_busy_compute_is_a_message(self, mock_wvm):
conn = self.refused(mock_wvm)
url = reverse("storage", args=[self.compute.id, "default"])
with patch("storages.views.libvirt_compute_lock", side_effect=claims.Busy("compute:1")):
response = self.client.post(url, {"del_volume": "1", "volname": "disk.qcow2"})
self.assertRedirects(response, url, fetch_redirect_response=False)
conn.del_volume.assert_not_called()
messages = [str(m) for m in get_messages(response.wsgi_request)]
self.assertTrue(any("short operation" in m for m in messages), messages)
@patch("storages.api.viewsets.wvmStorage")
def test_the_api_refuses_with_the_reason(self, mock_wvm):
conn = self.refused(mock_wvm)
conn.is_active.return_value = True
response = self.client.delete(f"/api/v1/computes/{self.compute.id}/storages/default/volumes/disk.qcow2/")
self.assertEqual(response.status_code, 409, response.content)
self.assertIn("disk.qcow2 is in use", response.json()["detail"])
@patch("storages.api.viewsets.wvmStorage")
def test_the_api_reads_no_volumes_of_a_stopped_pool(self, mock_wvm):
# it answered an empty list, or a made-up volume for any name
mock_wvm.return_value.is_active.return_value = False
base = f"/api/v1/computes/{self.compute.id}/storages/default/volumes/"
for url in (base, base + "anything.qcow2/"):
with self.subTest(url):
response = self.client.get(url)
self.assertEqual((response.status_code, response.json()), (409, {"detail": "Pool is not active"}))