"""A refused VM create form comes back bound and open, with what was entered; the other create forms keep their own defaults.""" import json 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='x"') self.assertEqual(page.get_element_by_id("custom_name").get("value"), 'x"') 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}"]) class CustomListsTests(TestCase): """The custom form's disks and networks come back with a refusal.""" def custom_lists(self, response): """The custom form's disks and networks the page restores, or None.""" element = html.fromstring(response.content).get_element_by_id("custom-lists", None) return None if element is None else json.loads(element.text) def test_a_refused_custom_form_sends_back_its_lists(self): _, response = render_create_page(self, valid( form_id="custom", name="bad name!", **{"storage-control": "default"}, images="a.qcow2,b.qcow2", device0="cdrom", bus0="sata", device1="disk", bus1="virtio", networks="default,isolated", mac="52:54:00:00:00:01")) self.assertEqual(self.custom_lists(response), { "pool": "default", "disks": [{"volume": "a.qcow2", "device": "cdrom", "bus": "sata"}, {"volume": "b.qcow2", "device": "disk", "bus": "virtio"}], "networks": [{"name": "default", "mac": "52:54:00:00:00:01"}, {"name": "isolated", "mac": ""}], }) def test_a_hostile_volume_name_round_trips_through_the_lists(self): name = "x'\">" _, response = render_create_page(self, valid(form_id="custom", name="bad name!", images=name)) self.assertEqual(self.custom_lists(response)["disks"][0]["volume"], name) self.assertNotIn(b"", response.content) def test_only_a_refused_custom_form_sends_lists(self): _, response = render_create_page(self) self.assertIsNone(self.custom_lists(response)) _, response = render_create_page(self, valid(form_id="template", name="bad name!", images="a.qcow2")) self.assertIsNone(self.custom_lists(response))