1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/instances/tests/test_create_form_input.py
catborise dba41f97f3 fix(instances): a refused VM create form comes back with what was entered
When the server refused a VM create submission (a field error, a busy
resource, a reused submission key, an existing VM name, a missing MAC
for a new disk), the page came back on its first tab with the flavor
dialog closed and every entered value gone: the hand-written fields
showed fixed defaults, and the errors sat in a block at the top of the
page, outside #messages and tied to no field.

The view builds one NewVMForm per create form on the page (custom,
template, one per flavor; create_forms), each with its own defaults; a
hidden form_id, which is also the prefix of the form's field ids, names
the submitted one, and it comes back bound. Every field renders from its
own form and is marked by two small template tags (form_fields:
error_attrs, field_error) when refused; a summary at the top of the
form lists all of its errors, also those of fields it keeps hidden. The
submitted tab or flavor dialog, marked data-form-id, reopens with any
"Advanced" section that holds an error; a reopened dialog shows the
messages too; the template form gets its pool, template and storage
back. base.html's error focus is a function that walks the places an
error can be, and a page can call it once it is set up, as the create
page does. form_id changes nothing in how a POST is processed; one of
an unknown form (a flavor deleted since) has its errors in the
messages. The default video now applies to every create form.

The custom form's disks and networks are not restored yet.
2026-10-20 08:12:25 +03:00

181 lines
10 KiB
Python

"""A refused VM create form comes back bound and open, with what was
entered; the other create forms keep their own defaults."""
import uuid
from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
from instances.models import Flavor
from instances.tests.create_page import render_create_page
from lxml import html
from operations import claims, jobs
from operations.models import Operation
from vrtManager import util
def valid(**fields):
"""A create submission NewVMForm accepts."""
return {"name": "vm1", "firmware": "BIOS", "vcpu": "1", "memory": "512", "networks": "default",
"cache_mode": "default", "add_cdrom": "None", "add_input": "None", "video": "vga",
"listener_addr": "0.0.0.0", "submission_key": uuid.uuid4().hex, **fields}
class CreateFormInputTests(TestCase):
def setUp(self):
self.flavor = Flavor.objects.create(label="wvc-test", memory=512, vcpu=1, disk=10)
def test_each_create_form_has_its_own_unbound_form(self):
_, response = render_create_page(self)
forms = response.context["vm_forms"]
self.assertEqual(set(forms), {"custom", "template"} | {f"flavor{f.id}" for f in Flavor.objects.all()})
self.assertFalse(any(form.is_bound for form in forms.values()))
self.assertEqual(response.context["submitted_form"], "")
def test_the_refused_form_comes_back_bound(self):
_, response = render_create_page(self, valid(form_id="custom", name="bad name!", vcpu="2"))
self.assertEqual(response.context["submitted_form"], "custom")
self.assertTrue(response.context["custom_form"].is_bound)
self.assertFalse(response.context["template_form"].is_bound)
self.assertIn("name", response.context["custom_form"].errors)
def test_a_refusal_after_validation_keeps_the_form(self):
for refusal in (claims.Busy("compute 1"), jobs.SubmissionConflict("already submitted"),
util.OperationError("A virtual machine with this name already exists")):
with self.subTest(refusal=type(refusal).__name__), \
patch("instances.views.jobs.submit", side_effect=refusal):
_, response = render_create_page(self, valid(form_id="template"))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.context["submitted_form"], "template")
self.assertTrue(response.context["template_form"].is_bound)
self.assertTrue(list(response.context["messages"]))
# the view's own refusal: a new disk needs a MAC
_, response = render_create_page(self, valid(form_id="template", hdd_size="10", mac=""))
self.assertEqual(response.context["submitted_form"], "template")
self.assertTrue(response.context["template_form"].is_bound)
def test_a_stale_form_id_is_no_form(self):
_, response = render_create_page(self, valid(form_id="flavor999", name="bad name!"))
self.assertEqual(response.context["submitted_form"], "")
# no form shows the error: it goes to the messages
self.assertTrue(any("special characters" in str(m) for m in response.context["messages"]))
def test_a_valid_post_with_a_stale_form_id_still_queues_the_job(self):
compute, response = render_create_page(self, valid(
form_id="flavor999", hdd_size="10", storage="default-pool", mac="52:54:00:00:00:01"))
self.assertRedirects(response, reverse("instances", args=[compute.id]), fetch_redirect_response=False)
self.assertEqual(Operation.objects.get().kind, "instance.create")
class CreateFormValuesTests(TestCase):
"""The custom and template forms render their own form's values and errors."""
def setUp(self):
Flavor.objects.create(label="wvc-test", memory=512, vcpu=1, disk=10)
def page(self, **fields):
_, response = render_create_page(self, valid(**fields))
self.assertEqual(response.status_code, 200)
return html.fromstring(response.content)
def summary(self, page, form_label):
return " ".join(page.xpath(f"//form[@aria-label='{form_label}']//*[contains(@class, 'form-errors')]//text()"))
def test_the_custom_form_keeps_what_was_submitted(self):
page = self.page(form_id="custom", name="bad name!", vcpu="3", memory="2048", cache_mode="writeback")
self.assertEqual(page.get_element_by_id("custom_name").get("value"), "bad name!")
self.assertEqual(page.get_element_by_id("custom_vcpu").get("value"), "3")
self.assertEqual(page.get_element_by_id("custom_memory").get("value"), "2048")
self.assertEqual(page.xpath("//select[@id='cache_mode']/option[@selected]/@value"), ["writeback"])
self.assertIsNone(page.get_element_by_id("custom_qemu_ga").get("checked")) # not submitted: unchecked
# the template form keeps its defaults
self.assertEqual(page.get_element_by_id("template_vcpu").get("value"), "1")
self.assertIsNotNone(page.get_element_by_id("template_qemu_ga").get("checked"))
def test_a_blank_number_stays_blank(self):
page = self.page(form_id="custom", vcpu="")
self.assertEqual(page.get_element_by_id("custom_vcpu").get("value"), "")
self.assertTrue(page.get_element_by_id("custom_vcpu_error").text_content().strip())
def test_the_name_is_escaped(self):
page = self.page(form_id="custom", name='<b>x</b>"')
self.assertEqual(page.get_element_by_id("custom_name").get("value"), '<b>x</b>"')
self.assertEqual(page.xpath("//form[@aria-label='Create custom instance form']//b"), [])
def test_the_error_sits_beside_its_field(self):
page = self.page(form_id="custom", name="bad name!")
field = page.get_element_by_id("custom_name")
self.assertIn("is-invalid", field.get("class").split())
self.assertEqual((field.get("aria-invalid"), field.get("aria-describedby")), ("true", "custom_name_error"))
self.assertIn("special characters", page.get_element_by_id("custom_name_error").text_content())
# and in the summary at the top of its form
self.assertIn("Name: The name of the virtual machine", self.summary(page, "Create custom instance form"))
def test_the_template_form_keeps_its_pool_template_and_storage(self):
page = self.page(form_id="template", name="bad name!", template_pool="default-pool",
template="base.img", storage="default-pool")
form = page.xpath("//form[@aria-label='Create instance from template form']")[0]
self.assertEqual((form.get("data-template-pool"), form.get("data-template"), form.get("data-storage")),
("default-pool", "base.img", "default-pool"))
self.assertEqual(page.get_element_by_id("template_disk").get("name"), "template_pool")
def test_an_error_of_a_field_the_form_hides_is_in_its_summary(self):
# the custom form's networks are a hidden input behind a picker
page = self.page(form_id="custom", networks="")
self.assertIn("Networks: This field is required.", self.summary(page, "Create custom instance form"))
def test_each_form_sends_its_own_form_id(self):
page = self.page(form_id="custom", name="bad name!")
for label, form_id in (("Create custom instance form", "custom"), ("Create instance from template form", "template")):
with self.subTest(form=form_id):
form = page.xpath(f"//form[@aria-label='{label}']")[0]
self.assertEqual(form.xpath(".//input[@name='form_id']/@value"), [form_id])
def test_a_firmware_error_without_a_firmware_choice_is_in_the_summary(self):
# an architecture with no firmware label renders no firmware select
with patch("vrtManager.connection.wvmConnect.label_for_firmware_path", return_value=None):
fields = valid(form_id="custom")
del fields["firmware"]
_, response = render_create_page(self, fields)
page = html.fromstring(response.content)
self.assertEqual(page.xpath("//select[@name='firmware']"), [])
self.assertIn("Firmware: This field is required.", self.summary(page, "Create custom instance form"))
class FlavorFormTests(TestCase):
"""Each flavor's modal form renders its own form; the refused one comes back."""
def setUp(self):
self.first = Flavor.objects.create(label="wvc-test-1", memory=512, vcpu=1, disk=10)
self.second = Flavor.objects.create(label="wvc-test-2", memory=1024, vcpu=2, disk=20)
def page(self, **fields):
_, response = render_create_page(self, valid(**fields))
self.assertEqual(response.status_code, 200)
return html.fromstring(response.content)
def test_only_the_refused_flavor_comes_back(self):
page = self.page(form_id=f"flavor{self.second.id}", name="bad name!", vcpu="2", memory="1024", hdd_size="20")
self.assertEqual(page.get_element_by_id(f"flavor{self.second.id}_name").get("value"), "bad name!")
self.assertEqual(page.get_element_by_id(f"flavor{self.first.id}_name").get("value"), "")
self.assertTrue(page.get_element_by_id(f"flavor{self.first.id}_mac").get("value"))
def test_a_flavor_field_points_at_its_error(self):
page = self.page(form_id=f"flavor{self.second.id}", name="bad name!", vcpu="2", memory="1024", hdd_size="20")
field = page.get_element_by_id(f"flavor{self.second.id}_name")
error = page.get_element_by_id(field.get("aria-describedby"))
self.assertIn("special characters", error.text_content())
def test_a_reopened_flavor_holds_the_message(self):
with patch("instances.views.jobs.submit",
side_effect=util.OperationError("A virtual machine with this name already exists")):
page = self.page(form_id=f"flavor{self.second.id}", vcpu="2", memory="1024", hdd_size="20",
storage="default-pool", mac="52:54:00:00:00:01")
messages = page.xpath(f"//*[@id='addVMflavor{self.second.id}']//*[contains(@class, 'modal-messages')]")
self.assertIn("A virtual machine with this name already exists", messages[0].text_content())
def test_the_flavor_form_sends_one_cache_mode(self):
page = self.page(form_id="custom", name="bad name!")
form = page.get_element_by_id(f"addVMflavor{self.first.id}").xpath(".//form")[0]
self.assertEqual(len(form.xpath(".//*[@name='cache_mode']")), 1)
self.assertEqual(form.xpath(".//input[@name='form_id']/@value"), [f"flavor{self.first.id}"])