Files
horizon/openstack_dashboard/test/selenium/integration/test_images.py
Jan Jasek efdf40fb46 pytest-based tests individual timeout for message check
Until now we have used implicit timeout for message checking.
It is enough for the majority of the cases. But in cases of
overloaded deployment and more consuming actions (like manage
volume attachments) this timeout is enough for all the actions
except for messages so individual message timeout (adjustable
through horizon.conf) seems to be the best solution.

Change-Id: I6e43cdfa9fc02c1346b5a8228caacaf154e20ba3
Signed-off-by: Jan Jasek <jjasek@redhat.com>
2025-07-30 21:42:17 +02:00

556 lines
23 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import os
import tempfile
import time
from oslo_utils import uuidutils
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from openstack_dashboard.test.selenium.integration import test_instances
from openstack_dashboard.test.selenium import widgets
# Imported fixtures
clear_instance_admin = test_instances.clear_instance_admin
@pytest.fixture(params=[1])
def image_names(request):
count = request.param
img_name_list = ['horizon_img_%s' % uuidutils.generate_uuid(dashed=False)]
if count > 1:
img_name_list = [f"{img_name_list[0]}-{item}"
for item in range(1, count + 1)]
return img_name_list
@pytest.fixture
def new_image_demo(image_names, temporary_file, openstack_demo):
for img in image_names:
image = openstack_demo.create_image(
img,
disk_format="raw",
filename=temporary_file,
wait=True,
)
yield image
for img in image_names:
openstack_demo.delete_image(img)
@pytest.fixture
def new_image_admin(image_names, temporary_file, openstack_admin):
for img in image_names:
image = openstack_admin.create_image(
img,
disk_format="raw",
filename=temporary_file,
wait=True,
)
yield image
for img in image_names:
openstack_admin.delete_image(img)
@pytest.fixture
def new_protected_image_admin(image_names, temporary_file, openstack_admin):
for img in image_names:
image = openstack_admin.create_image(
img,
disk_format="raw",
filename=temporary_file,
is_protected=True,
wait=True,
)
yield image
for img in image_names:
openstack_admin.delete_image(img)
@pytest.fixture
def clear_image_demo(image_names, openstack_demo):
yield None
for img in image_names:
openstack_demo.delete_image(
img,
wait=True,
)
@pytest.fixture
def clear_image_admin(image_names, openstack_admin):
yield None
for img in image_names:
openstack_admin.delete_image(
img,
wait=True,
)
@pytest.fixture
def temporary_file(tmp_path):
"""Generate temporary file.
:return: path to the generated file
"""
with tempfile.NamedTemporaryFile(suffix='.qcow2',
dir=tmp_path) as tmp_file:
tmp_file.write(os.urandom(5000))
yield tmp_file.name
def wait_for_steady_state_of_unprotected_image(openstack, image_name):
for attempt in range(3):
image_attributes_details = openstack.image.find_image(image_name)
if image_attributes_details["status"] == "active" \
and image_attributes_details["protected"] is False:
break
else:
time.sleep(2)
def wait_for_angular_readiness(driver, config):
driver.set_script_timeout(config.selenium.page_timeout)
driver.execute_async_script("""
var callback = arguments[arguments.length - 1];
var element = document.querySelector('div.btn-group[name="protected"]');
if (!window.angular) {
callback(false)
}
if (angular.getTestability) {
angular.getTestability(element).whenStable(function(){callback(true)});
} else {
if (!angular.element(element).injector()) {
callback(false)
}
var browser = angular.element(element).injector().get('$browser');
browser.notifyWhenNoOutstandingRequests(function(){callback(true)});
};""")
def test_image_create_from_local_file_demo(login, driver, image_names,
temporary_file, clear_image_demo,
config, openstack_demo):
image_name = image_names[0]
login('user')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
driver.find_element_by_xpath(
"//button[normalize-space()='Create Image']").click()
wizard = driver.find_element_by_css_selector("wizard")
wizard.find_element_by_id("imageForm-name").send_keys(image_name)
select_element = wizard.find_element_by_css_selector(
"input[name='image_file']")
select_element.send_keys(temporary_file)
wizard.find_element_by_id("imageForm-format").click()
wizard.find_element_by_css_selector(
"[label='QCOW2 - QEMU Emulator']").click()
wizard.find_element_by_css_selector("button.btn-primary.finish").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert (f"Success: Image {image_name} was successfully"
f" created." in messages)
assert openstack_demo.compute.find_image(image_name) is not None
def test_image_delete_demo(login, driver, image_names, openstack_demo,
new_image_demo, config):
image_name = image_names[0]
login('user')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Delete Image")
widgets.confirm_modal(driver)
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Success: Deleted Image: {image_name}." in messages
assert openstack_demo.compute.find_image(image_name) is None
@pytest.mark.parametrize('image_names', [2], indirect=True)
def test_image_pagination_demo(login, driver, image_names, openstack_demo,
change_page_size_demo, new_image_demo, config):
items_per_page = 1
img_list = sorted([item["name"]
for item in openstack_demo.compute.images()])
first_page_definition = widgets.TableDefinition(next=True, prev=False,
count=items_per_page,
names=[img_list[0]])
second_page_definition = widgets.TableDefinition(next=True, prev=True,
count=items_per_page,
names=[img_list[1]])
third_page_definition = widgets.TableDefinition(next=False, prev=True,
count=items_per_page,
names=[img_list[2]])
login('user')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
actual_page1_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert first_page_definition == actual_page1_definition
# Turning to next page(page2)
driver.find_element_by_link_text("Next »").click()
actual_page2_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert second_page_definition == actual_page2_definition
# Turning to next page(page3)
driver.find_element_by_link_text("Next »").click()
actual_page3_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert third_page_definition == actual_page3_definition
# Turning back to previous page(page2)
driver.find_element_by_link_text("« Prev").click()
actual_page2_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert second_page_definition == actual_page2_definition
# Turning back to previous page(page1)
driver.find_element_by_link_text("« Prev").click()
actual_page1_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert first_page_definition == actual_page1_definition
# Admin tests
def test_image_create_from_local_file_admin(login, driver, image_names,
temporary_file, clear_image_admin,
config, openstack_admin):
image_name = image_names[0]
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
driver.find_element_by_xpath(
"//button[normalize-space()='Create Image']").click()
wizard = driver.find_element_by_css_selector("wizard")
wizard.find_element_by_id("imageForm-name").send_keys(image_name)
select_element = wizard.find_element_by_css_selector(
"input[name='image_file']")
select_element.send_keys(temporary_file)
wizard.find_element_by_id("imageForm-format").click()
wizard.find_element_by_css_selector(
"[label='QCOW2 - QEMU Emulator']").click()
wizard.find_element_by_css_selector("button.btn-primary.finish").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert (f"Success: Image {image_name} was successfully"
f" created." in messages)
assert openstack_admin.compute.find_image(image_name) is not None
def test_image_delete_admin(login, driver, image_names, openstack_admin,
new_image_admin, config):
image_name = image_names[0]
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Delete Image")
widgets.confirm_modal(driver)
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Success: Deleted Image: {image_name}." in messages
assert openstack_admin.compute.find_image(image_name) is None
@pytest.mark.parametrize('image_names', [2], indirect=True)
def test_image_pagination_admin(login, driver, image_names, openstack_admin,
change_page_size_admin, new_image_admin,
config):
items_per_page = 1
img_list = sorted([item["name"]
for item in openstack_admin.compute.images()])
first_page_definition = widgets.TableDefinition(next=True, prev=False,
count=items_per_page,
names=[img_list[0]])
second_page_definition = widgets.TableDefinition(next=True, prev=True,
count=items_per_page,
names=[img_list[1]])
third_page_definition = widgets.TableDefinition(next=False, prev=True,
count=items_per_page,
names=[img_list[2]])
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
actual_page1_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert first_page_definition == actual_page1_definition
# Turning to next page(page2)
driver.find_element_by_link_text("Next »").click()
actual_page2_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert second_page_definition == actual_page2_definition
# Turning to next page(page3)
driver.find_element_by_link_text("Next »").click()
actual_page3_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert third_page_definition == actual_page3_definition
# Turning back to previous page(page2)
driver.find_element_by_link_text("« Prev").click()
actual_page2_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert second_page_definition == actual_page2_definition
# Turning back to previous page(page1)
driver.find_element_by_link_text("« Prev").click()
actual_page1_definition = widgets.get_image_table_definition(driver,
sorting=True)
assert first_page_definition == actual_page1_definition
def test_image_filtration_admin(login, driver, new_image_admin, config):
image_name = new_image_admin.name
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
filter_input_field = driver.find_element_by_css_selector(".search-input")
filter_input_field.send_keys(image_name)
# Fetch page definition after filtration
current_page_definition = widgets.get_image_table_definition(driver)
assert vars(current_page_definition)['names'][0].text == image_name
assert vars(current_page_definition)['count'] == 1
filter_input_field.clear()
# Generate random non existent image name
random_img_name = 'horizon_img_%s' % uuidutils.generate_uuid(dashed=False)
filter_input_field.send_keys(random_img_name)
# Fetch page definition after filtration
no_items_present = driver.find_element_by_xpath(
"//*[normalize-space()='No items to display.']")
assert no_items_present
def test_remove_protected_image_admin(login, driver, image_names,
new_protected_image_admin, config,
openstack_admin):
image_name = new_protected_image_admin.name
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
menu_button = actions_column.find_element_by_css_selector(
".dropdown-toggle"
)
menu_button.click()
options = actions_column.find_elements_by_css_selector(
"ul.dropdown-menu li")
for option in options:
if option.text == "Delete Image":
pytest.fail("Delete option should not exist")
actions_column.find_element_by_xpath(
".//*[normalize-space()='Edit Image']").click()
wait_for_angular_readiness(driver, config)
image_form = driver.find_element_by_css_selector(".ng-wizard")
image_form.find_element_by_xpath(".//label[text()='No']").click()
image_form.find_element_by_xpath(
".//button[@class='btn btn-primary finish']").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Success: Image {image_name} was successfully updated." in messages
wait_for_steady_state_of_unprotected_image(openstack_admin, image_name)
WebDriverWait(driver, config.selenium.page_timeout).until(
EC.invisibility_of_element(
(By.CLASS_NAME, "modal modal-dialog-wizard fade ng-scope "
"ng-isolate-scope ng-animate ng-leave ng-leave-active")))
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Delete Image")
widgets.confirm_modal(driver)
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Success: Deleted Image: {image_name}." in messages
assert openstack_admin.compute.find_image(image_name) is None
def test_edit_image_description_admin(login, driver, image_names,
new_image_admin, config,
openstack_admin):
image_name = new_image_admin.name
new_description = "new_description_text"
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Edit Image")
wait_for_angular_readiness(driver, config)
image_form = driver.find_element_by_css_selector(".ng-wizard")
desc_field = image_form.find_element_by_css_selector(
"#imageForm-description")
desc_field.clear()
desc_field.send_keys(new_description)
image_form.find_element_by_xpath(
".//button[@class='btn btn-primary finish']").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Success: Image {image_name} " \
f"was successfully updated." in messages
image_id = new_image_admin.id
assert (openstack_admin.compute.get(f"/images/{image_id}").json(
)['image']['metadata']['description'] == new_description)
def test_update_image_metadata_admin(login, driver,
new_image_admin, config,
openstack_admin):
new_metadata = {
'metadata1': 'img_metadata%s' % uuidutils.generate_uuid(dashed=False),
'metadata2': 'img_metadata%s' % uuidutils.generate_uuid(dashed=False)
}
image_name = new_image_admin.name
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Update Metadata")
image_form = driver.find_element_by_css_selector(".modal-content")
for name, value in new_metadata.items():
image_form.find_element_by_xpath(
"//input[@name='customItem']").send_keys(name)
image_form.find_element_by_css_selector(
"button.btn span[class='fa fa-plus']").click()
image_form.find_element_by_xpath(
f"//span[@title='{name}']/parent::div/input").send_keys(value) # noqa: E231,E501
image_form.find_element_by_xpath(
"//button[@ng-click='modal.save()']").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert "Success: Metadata was successfully updated." in messages
image_id = new_image_admin.id
for name, value in new_metadata.items():
assert (openstack_admin.compute.get(f"/images/{image_id}").json(
)['image']['metadata'][name] == value)
def test_launch_instance_from_image_admin(complete_default_test_network, login,
driver, instance_name,
clear_instance_admin, new_image_admin,
config, openstack_admin):
image_name = new_image_admin.name
network = complete_default_test_network.name
flavor = config.launch_instances.flavor
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
rows[0].find_element_by_xpath(
"//ng-transclude[normalize-space()='Launch']").click()
wizard = driver.find_element_by_css_selector("wizard")
navigation = wizard.find_element_by_css_selector("div.wizard-nav")
widgets.find_already_visible_element_by_xpath(
".//*[@id='name']", wizard).send_keys(instance_name)
navigation.find_element_by_link_text("Networks").click()
network_table = wizard.find_element_by_css_selector(
"ng-include[ng-form=launchInstanceNetworkForm]"
)
widgets.select_from_transfer_table(network_table, network)
navigation.find_element_by_link_text("Flavor").click()
flavor_table = wizard.find_element_by_css_selector(
"ng-include[ng-form=launchInstanceFlavorForm]"
)
widgets.select_from_transfer_table(flavor_table, flavor)
navigation.find_element_by_link_text("Source").click()
source_table = wizard.find_element_by_css_selector(
"ng-include[ng-form=launchInstanceSourceForm]"
)
test_instances.delete_volume_on_instance_delete(source_table, "Yes")
wizard.find_element_by_css_selector(
"button.btn-primary.finish").click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert "Info: Scheduled creation of 1 instance." in messages
assert openstack_admin.compute.find_server(instance_name) is not None
def test_create_volume_from_image_admin(login, driver, volume_name,
new_image_admin, clear_volume_admin,
config, openstack_admin):
volume_name = volume_name[0]
image_name = new_image_admin.name
login('admin')
url = '/'.join((
config.dashboard.dashboard_url,
'project',
'images',
))
driver.get(url)
rows = driver.find_elements_by_xpath(f"//a[text()='{image_name}']")
assert len(rows) == 1
actions_column = rows[0].find_element_by_xpath(
".//ancestor::tr/td[contains(@class,'actions_column')]")
widgets.select_from_dropdown(actions_column, "Create Volume")
name_field = driver.find_element_by_xpath("//input[@name='name']")
name_field.clear()
name_field.send_keys(volume_name)
create_vol_btn = WebDriverWait(driver, config.selenium.page_timeout).until(
EC.element_to_be_clickable(
(By.XPATH, "//button[@class='btn btn-primary finish']")))
create_vol_btn.click()
messages = widgets.get_and_dismiss_messages(driver, config)
assert f"Info: Creating volume {volume_name}" in messages
assert openstack_admin.block_storage.find_volume(volume_name) is not None