From b589cbb9a8deceb8093bf034a077bbcc252d8c9b Mon Sep 17 00:00:00 2001 From: Jelle van der Waa Date: Tue, 4 Feb 2025 14:17:05 +0100 Subject: [PATCH] test: amplify test permissions --- test/check-application | 2340 +--------------------------------------- test/run | 1 - 2 files changed, 17 insertions(+), 2324 deletions(-) diff --git a/test/check-application b/test/check-application index f37292f0..be025b74 100755 --- a/test/check-application +++ b/test/check-application @@ -237,1082 +237,31 @@ class TestFiles(testlib.MachineCase): b.go("/files#/?path=etc") b.wait_text("li[data-location='/etc']", "etc") - def testNavigation(self) -> None: - b = self.browser - m = self.machine + def testPermissions1(self) -> None: + self._testPermissions() - self.enter_files() + def testPermissions2(self) -> None: + self._testPermissions() - b.wait_text("li[data-location='/home']", "home") - b.wait_text("li[data-location='/home/admin']", "admin") - - # clicking on the home button should take us to the home directory - b.click("li[data-location='/home'] a") - b.wait_visible("li[data-location='/home'] a.pf-m-current") - b.wait_text("li[data-location='/home']", "home") - b.wait_visible("[data-item='admin']") - - dir_cnt = int(m.execute(r''' - find /home -mindepth 1 -maxdepth 1 -type d \( -name ".*" -prune -o -print \) | wc -l - ''').strip()) - hidden_cnt = int(m.execute(r'ls -A /home | grep "^\." | wc -l').strip()) - # -1 to ignore first line of output which is not a file - files_cnt = int(m.execute(r'ls -lA /home | wc -l').strip()) - dir_cnt - hidden_cnt - 1 - b.wait_in_text(".files-footer-info", - f"Directory contains {dir_cnt} directories, {files_cnt} files, {hidden_cnt} hidden") - - # double-clicking on a directory should take us into it - b.mouse("[data-item='admin']", "dblclick") - b.wait_not_present("[data-item='admin']") - self.assert_last_breadcrumb("admin") + def testPermissions3(self) -> None: + self._testPermissions() - # cwd info is updated when navigating to a new directory - dir_cnt = int(m.execute(r''' - find /home/admin -mindepth 1 -maxdepth 1 -type d \( -name ".*" -prune -o -print \) | wc -l - ''').strip()) - hidden_cnt = int(m.execute(r'ls -A /home/admin | grep "^\." | wc -l').strip()) - files_cnt = int(m.execute(r'ls -lA /home/admin | wc -l').strip()) - dir_cnt - hidden_cnt - 1 - b.wait_in_text(".files-footer-info", - f"Directory contains {dir_cnt} directories, {files_cnt} files, {hidden_cnt} hidden") - - # Enabling "show hidden files" changes the counters in files footer - dir_cnt = int(m.execute("ls -lA /home/admin | grep '^d' | wc -l").strip()) - files_cnt = int(m.execute("ls -lA /home/admin | grep '^-' | wc -l").strip()) - b.select_PF("#sort-menu-toggle", "Show hidden items") - b.wait_in_text(".files-footer-info", - f"Directory contains {dir_cnt} directories, {files_cnt} files") - b.select_PF("#sort-menu-toggle", "Hide hidden items") + def testPermissions4(self) -> None: + self._testPermissions() - # double-clicking on a symlink to a directory also takes us into it - m.execute("ln -s /tmp /home/admin/tmplink") - b.mouse("[data-item='tmplink']", "dblclick") - b.wait_not_present("[data-item='tmplink']") - self.assert_last_breadcrumb("tmplink") - b.go("/files#/?path=/home/admin") - b.wait_not_present(".pf-v5-c-empty-state") + def testPermissions5(self) -> None: + self._testPermissions() - # create folders and test navigation history buttons - m.execute("mkdir /home/admin/newdir") - m.execute("mkdir /home/admin/newdir/newdir2") - b.mouse("[data-item='newdir']", "dblclick") - b.wait_not_present("[data-item='admin']") - b.wait_visible("[data-item='newdir2']") - b.mouse("[data-item='newdir2']", "dblclick") - b.wait_not_present("[data-item='newdir']") - b.click("li[data-location='/home'] a") - self.assert_last_breadcrumb("home") - b.wait_visible("[data-item='admin']") - # navigate back - b.eval_js("window.history.back()") - self.assert_last_breadcrumb("newdir2") - b.wait_not_present("[data-item='admin']") - b.eval_js("window.history.back()") - self.assert_last_breadcrumb("newdir") - b.wait_visible("[data-item='newdir2']") - b.eval_js("window.history.back()") - self.assert_last_breadcrumb("admin") - b.wait_visible("[data-item='newdir']") - # navigate forward - b.eval_js("window.history.forward()") - b.wait_not_present("[data-item='admin']") - self.assert_last_breadcrumb("newdir") - b.eval_js("window.history.forward()") - b.wait_not_present("[data-item='newdir']") - self.assert_last_breadcrumb("newdir2") - b.eval_js("window.history.forward()") - # Switching navigation resets selected state - b.wait_visible("[data-item='admin']") - b.wait_not_present("[data-item='admin'].row-selected") - b.wait_not_present("[data-item='newdir']") - self.assert_last_breadcrumb("home") - b.wait_visible("[data-item='admin']") - - # Change permissions of cwd and see that files-footer-info is updated - b.go("/files#/?path=/home/admin/newdir") - self.assert_last_breadcrumb("newdir") - b.wait_in_text("#files-footer-permissions", "rwx r-x r-x") - m.execute("chmod 700 /home/admin/newdir") - b.wait_in_text("#files-footer-permissions", "rwx --- ---") - b.wait_text("#files-footer-owner", "root") - - # Also check popover - b.click("#files-footer-permissions") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(1) > dd", "read, write, and execute") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(2) > dd", "none") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(3) > dd", "none") - b.click(".pf-v5-c-popover__close > button") - b.wait_not_present(".pf-v5-c-popover") - - b.click("#files-footer-owner") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(1) > dd", "root") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(2) > dd", "root") - b.click(".pf-v5-c-popover__close > button") - b.wait_not_present(".pf-v5-c-popover") - - # Change group is shown in popover - m.execute("chown root:admin /home/admin/newdir") - b.wait_text("#files-footer-owner", "root:admin") - - b.click("#files-footer-owner") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(1) > dd", "root") - b.wait_in_text(".pf-v5-c-popover dl div:nth-child(2) > dd", "admin") - b.click(".pf-v5-c-popover__close > button") - b.wait_not_present(".pf-v5-c-popover") - - # Change last edit time on cwd - m.execute("touch -d '2 hours ago' /home/admin/newdir") - b.wait_in_text(".files-footer-mtime", "2 hours ago") - m.execute("touch /home/admin/newdir") - b.wait_in_text(".files-footer-mtime", "less than a minute ago") - # mock edit time for tooltip - m.execute("touch -d '1995/12/21' /home/admin/newdir") - b.mouse(".files-footer-mtime", "mouseenter") - b.wait_in_text(".pf-v5-c-tooltip", "Dec 21, 1995") - b.mouse(".files-footer-mtime", "mouseleave") - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") - - # Navigating resets the current search filter - b.go("/files#/?path=/") - self.assert_last_breadcrumb('/') - b.set_input_text("input[placeholder='Filter directory']", "sys") - self.browser.wait_js_cond("ph_count('#folder-view tbody tr') == 1") - - b.mouse("[data-item='sys']", "dblclick") - self.assert_last_breadcrumb("sys") - b.wait_val("input[placeholder='Filter directory']", "") - - b.set_input_text("input[placeholder='Filter directory']", "no-matches-at-all") - self.browser.wait_js_cond("ph_count('#folder-view tbody tr') == 0") - self.assert_last_breadcrumb("sys") - - b.click("li[data-location='/'] a") - self.assert_last_breadcrumb("/") - b.wait_val("input[placeholder='Filter directory']", "") + def testPermissions6(self) -> None: + self._testPermissions() - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") + def testPermissions7(self) -> None: + self._testPermissions() - # Selecting one file shows its info in the footer - b.click("[data-item='newdir']") - b.wait_in_text(".files-footer-info", "newdir") - b.wait_in_text("#files-footer-permissions", "rwx --- ---") - b.wait_in_text("#files-footer-owner", "root") - - # Select multiple files - m.execute("touch /home/admin/newfile.txt") - b.mouse("[data-item='newfile.txt']", "click", ctrlKey=True) - b.wait_in_text(".files-footer-info", "2 files selected") - b.mouse("[data-item='tmplink']", "click", ctrlKey=True) - b.wait_in_text(".files-footer-info", "3 files selected") - b.go("/files#/?path=/home") - self.assert_last_breadcrumb("home") - - # Navigation via editing the path - path_input = "#new-path-input" - edit_button = ".breadcrumb-button-edit" - apply_button = ".breadcrumb-button-edit-apply" - cancel_button = ".breadcrumb-button-edit-cancel" - - # Cancel - - # Via escape - b.click(edit_button) - b.wait_val(path_input, "/home/") - b.set_input_text(path_input, "/home/admin") - b.wait_visible(path_input) - b.focus(path_input) - b.key("Escape") - b.wait_not_present(path_input) - - # Via cancel button - b.click(edit_button) - # Cancelled edit should not save the path - b.wait_val(path_input, "/home/") - b.click(cancel_button) - b.wait_not_present(path_input) - - # Change path - - # Via Enter key - b.click(edit_button) - b.set_input_text(path_input, "/opt") - b.focus(path_input) - b.key("Enter") - self.assert_last_breadcrumb("opt") - - # Via apply button - b.click(edit_button) - b.set_input_text(path_input, "/var") - b.click(apply_button) - self.assert_last_breadcrumb("var") - - # Editing and cancelling does not remember input - b.click(edit_button) - b.set_input_text(path_input, "/path/to/nowhere") - b.click(cancel_button) - b.wait_not_present(path_input) - b.click(edit_button) - b.wait_visible(path_input) - b.wait_val(path_input, "/var/") - b.click(cancel_button) - - # Editing / shows / in the input - b.click(edit_button) - b.set_input_text(path_input, "/") - b.click(apply_button) - self.assert_last_breadcrumb("/") - b.click(edit_button) - b.wait_val(path_input, "/") - b.click(cancel_button) + def testPermissions8(self) -> None: + self._testPermissions() - def testSorting(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # set a bogus sort value in localStorage to make sure we handle it gracefully - b.eval_js("""window.localStorage.setItem("files:sort", 'bzzt')""") - b.reload() - self.enter_files() - - # Expected heading - b.wait_visible(".header-toolbar") - - # Create test files and folders - m.execute("touch -d '3 hours ago' /home/admin/aaa") - b.wait_visible("[data-item='aaa']") - m.execute("touch -d '4 hours ago' /home/admin/BBB") - b.wait_visible("[data-item='BBB']") - m.execute("touch -d '2 hours ago' /home/admin/ccc") - b.wait_visible("[data-item='ccc']") - - # Pixel test the menu - b.click("#sort-menu-toggle") - b.assert_pixels("#sort-menu", "sort-menu") - b.click("#sort-menu-toggle") - b.wait_not_present("#sort-menu") - - # Default sort is A-Z (also used for invalid value found in localStorage) - # Alphabet sorts should be case insensitive - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ccc") - - # Sort by reverse alphabet - b.select_PF("#sort-menu-toggle", "Z-A") - # Alphabet sorts should be case insensitive - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") - - # Sort by last modified - b.select_PF("#sort-menu-toggle", "Last modified") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") - - # Update content of files - m.execute('echo "update" > /home/admin/aaa') - - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") - - # Sort by first modified - b.select_PF("#sort-menu-toggle", "First modified") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") - - # Sort option should be saved in localStorage - b.select_PF("#sort-menu-toggle", "Z-A") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") - b.reload() - b.enter_page("/files") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "aaa") - - # Sort on size - m.execute(""" - echo 'lol' > /home/admin/aaa - sleep 0.01 # make sure these get different timestamps on the filesystem - truncate -s 10M /home/admin/BBB - """) - b.select_PF("#sort-menu-toggle", "Largest size") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ccc") - - b.select_PF("#sort-menu-toggle", "Smallest size") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "BBB") - - m.execute(""" - ln -s /tmp /home/admin/ddd - sleep 0.01 # make sure these get different timestamps on the filesystem - mkdir /home/admin/eee - sleep 0.01 # make sure these get different timestamps on the filesystem - mkdir /home/admin/Eee - """) - b.wait_visible("[data-item='ddd']") - b.wait_visible("[data-item='eee']") - b.wait_visible("[data-item='Eee']") - - # Directories are sorted first - b.select_PF("#sort-menu-toggle", "A-Z") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") - - # Sort headers also work in the list view - b.click("button[aria-label='Display as a list']") - b.assert_pixels("#files-card-parent", "list-view", mock={".item-date": "Jun 19, 2024, 11:30 AM"}) - b.wait_visible("th[aria-sort='ascending'].pf-m-selected button:contains(Name)") - - # clicking reverse sort order - b.click("th.pf-m-selected button") - b.wait_visible("th[aria-sort='descending'].pf-m-selected button:contains(Name)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa") - - b.click("th button:contains(Modified)") - b.wait_visible("th[aria-sort='ascending'].pf-m-selected button:contains(Modified)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB") - - b.click("th button:contains(Size)") - b.wait_visible("th[aria-sort='ascending'].pf-m-selected button:contains(Size)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") - - # Test sorting by permissions - basedir = "/home/admin" - m.execute(f""" - chmod 0754 {basedir}/eee - chmod 0755 {basedir}/Eee - chmod 0500 {basedir}/BBB - chmod 0477 {basedir}/aaa - chmod 0511 {basedir}/ccc - """) - - b.click("th button:contains(Permissions)") - b.wait_visible("th[aria-sort='ascending'].pf-m-selected button:contains(Permissions)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa") - - # Directories are sorted first - m.execute(f""" - chmod 0755 {basedir}/eee - chmod 0123 {basedir}/Eee - chmod 0777 {basedir}/BBB - chmod 0640 {basedir}/aaa - chmod 0600 {basedir}/ccc - """) - - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") - - # Clicking again inverts the list - # Directories are still sorted first - b.click("th button:contains(Permissions)") - b.wait_visible("th[aria-sort='descending'].pf-m-selected button:contains(Permissions)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB") - - # Special bits are not used for sorting - m.execute(f""" - chmod 6755 {basedir}/eee - chmod 0755 {basedir}/Eee - chmod 1700 {basedir}/BBB - chmod 3700 {basedir}/aaa - chmod 0701 {basedir}/ccc - """) - - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") - - # create test users - m.execute(""" - useradd barusr - useradd cmanusr - useradd foousr - useradd qusr - useradd rebelusr - """) - - # Sorting by file ownership - # Files are primarily sorted by user - m.execute(f""" - chown admin:root {basedir}/aaa - chown foousr:root {basedir}/BBB - chown barusr:root {basedir}/ccc - chown qusr:foousr {basedir}/eee - chown rebelusr:cmanusr {basedir}/Eee - """) - b.click("th button:contains(Owner)") - - # Verify order - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB") - - # Verify owner is displayed correctly - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-owner", "qusr:foousr") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-owner", "rebelusr:cmanusr") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-owner", "root") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-owner", "admin:root") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-owner", "barusr:root") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-owner", "foousr:root") - - # Reverse sorting - b.click("th button:contains(Owner)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa") - - # Files are sorted by group when user is the same - m.execute(f""" - chown admin:root {basedir}/aaa - chown admin:foousr {basedir}/BBB - chown admin:rebelusr {basedir}/ccc - chown --no-dereference admin:rebelusr {basedir}/ddd - chown admin:foousr {basedir}/eee - chown admin:admin {basedir}/Eee - """) - b.click("th button:contains(Owner)") - - # Verify order - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa") - - # Verify owner is displayed correctly - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-owner", "admin") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-owner", "admin:foousr") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-owner", "admin:rebelusr") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-owner", "admin:foousr") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-owner", "admin:rebelusr") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-owner", "admin:root") - - # Reverse sorting - b.click("th button:contains(Owner)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB") - - # Sorting falls back to file name sort - m.execute(f""" - chown admin:admin {basedir}/aaa - chown admin:admin {basedir}/BBB - chown admin:admin {basedir}/ccc - chown admin:admin {basedir}/eee - chown admin:admin {basedir}/Eee - """) - b.click("th button:contains(Owner)") - - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "ccc") - - # Sorting when UID is a number - # Numbers are sorted after known names - m.execute(f""" - chown foousr:admin {basedir}/aaa - chown foousr:568 {basedir}/BBB - chown foousr:admin {basedir}/ccc - chown foousr:admin {basedir}/eee - chown 569:admin {basedir}/Eee - chown --no-dereference 568:admin {basedir}/ddd - """) - - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "aaa") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "BBB") - - # Verify owner is displayed correctly - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-owner", "foousr:admin") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-owner", "568:admin") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-owner", "569:admin") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-owner", "foousr:admin") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-owner", "foousr:admin") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-owner", "foousr:568") - - # Reverse - b.click("th button:contains(Owner)") - b.wait_text("#folder-view tbody tr:nth-of-type(1) .item-name", "Eee") - b.wait_text("#folder-view tbody tr:nth-of-type(2) .item-name", "ddd") - b.wait_text("#folder-view tbody tr:nth-of-type(3) .item-name", "eee") - b.wait_text("#folder-view tbody tr:nth-of-type(4) .item-name", "BBB") - b.wait_text("#folder-view tbody tr:nth-of-type(5) .item-name", "ccc") - b.wait_text("#folder-view tbody tr:nth-of-type(6) .item-name", "aaa") - - def testDelete(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - self.allow_journal_messages("rm: cannot remove '/home/admin/newdir/newfile': Permission denied", - "rm: cannot remove '/home/admin/newfile': Operation not permitted") - - # Delete file - m.execute("touch /home/admin/newfile") - b.wait_visible("[data-item='newfile']") - self.delete_item("file", "newfile") - - # Delete file with space in the file name - m.execute(r"touch /home/admin/new\ file") - b.wait_visible("[data-item='new file']") - self.delete_item("file", "new file") - - # Delete empty directory - m.execute("mkdir /home/admin/newdir") - b.wait_visible("[data-item='newdir']") - self.delete_item("directory", "newdir") - - # Delete full directory - m.execute("mkdir /home/admin/newdir") - m.execute("touch /home/admin/newdir/newfile") - b.wait_visible("[data-item='newdir']") - self.delete_item("directory", "newdir") - - # Delete symlink - m.execute(""" - touch /home/admin/target - ln -s /home/admin/target /home/admin/link - """) - self.delete_item("link", "link") - - # Deleting protected file should give an error - m.execute("touch /home/admin/newfile") - m.execute("chattr +i /home/admin/newfile") - b.wait_visible("[data-item='newfile']") - self.delete_item("file", "newfile", expect_success=False) - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Force delete file newfile?") - b.assert_pixels(".pf-v5-c-modal-box", "delete-modal-error") - self.wait_modal_inline_alert("rm: cannot remove '/home/admin/newfile': Operation not permitted") - b.click("button.pf-m-danger") - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Force delete file newfile?") - self.wait_modal_inline_alert("rm: cannot remove '/home/admin/newfile': Operation not permitted") - b.click("div.pf-v5-c-modal-box__close button") - b.wait_not_present(".pf-v5-c-modal-box") - b.wait_visible("[data-item='newfile']") - m.execute("chattr -i /home/admin/newfile") - self.delete_item("file", "newfile") - - # Delete using keyboard shortcut - m.execute("touch /home/admin/delete1 /home/admin/delete2") - b.click("[data-item='delete1']") - b.mouse("[data-item='delete2']", "click", ctrlKey=True) - b.wait_visible("[data-item='delete1'].row-selected") - b.wait_visible("[data-item='delete2'].row-selected") - b.focus("#files-card-parent") - # For strange reasons ctrlKey remains pressed after the b.mouse() above (spotted in firefox) - b.key("Control") - b.key("Delete") - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Delete 2 items?") - b.assert_pixels(".pf-v5-c-modal-box", "delete-modal") - b.click("button.pf-m-danger") - - b.wait_not_present(".pf-v5-c-modal-box") - b.wait_not_present("[data-item='delete1']") - b.wait_not_present("[data-item='delete2']") - - def testCreateDirectory(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # Create folder - self.create_directory("newdir") - b.wait_visible("[data-item='newdir']") - self.assert_owner('/home/admin/newdir', 'admin:admin') - - # validation - b.click("#dropdown-menu") - b.click("#create-folder") - b.assert_pixels(".pf-v5-c-modal-box", "create-modal") - b.set_input_text("#create-directory-input", "test") - b.set_input_text("#create-directory-input", "") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#create-directory-input-helper", "Directory name cannot be empty") - - b.set_input_text("#create-directory-input", "a" * 256) - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#create-directory-input-helper", "Directory name too long") - - b.set_input_text("#create-directory-input", "foo/bar") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#create-directory-input-helper", "Directory name cannot include a /") - - b.set_input_text("#create-directory-input", "test") - b.wait_visible("button.pf-m-primary:not(:disabled)") - b.click(".pf-v5-c-modal-box__footer button.pf-m-link") # cancel - - # Creating folder with duplicate name should return an error - self.create_directory("newdir", expect_success=False) - self.wait_modal_inline_alert("mkdir: cannot create directory ‘/home/admin/newdir’: File exists") - b.click("div.pf-v5-c-modal-box__close button.pf-v5-c-button") - - # Creating folder with empty name should return an error - self.create_directory("", expect_success=False) - self.wait_modal_inline_alert("mkdir: cannot create directory ‘/home/admin/’: File exists") - b.click("div.pf-v5-c-modal-box__close button.pf-v5-c-button") - - # Creating folder inside protected folder should return an error - m.execute("chattr +i /home/admin/newdir") - self.addCleanup(m.execute, "chattr -i /home/admin/newdir") - b.mouse("[data-item='newdir']", "dblclick") - b.wait_not_present("[data-item='newdir']") - self.create_directory("test", expect_success=False) - alert_text = "mkdir: cannot create directory ‘/home/admin/newdir/test’: Operation not permitted" - self.wait_modal_inline_alert(alert_text) - b.click("div.pf-v5-c-modal-box__close button.pf-v5-c-button") - - # Creating folder as superuser a non-logged in owned directory has the expected folder permissions - b.go("/files#/?path=/root") - self.assert_last_breadcrumb("root") - self.addCleanup(m.execute, ['rm', '-rf', '/root/newdir']) - self.create_directory("newdir") - b.wait_visible("[data-item='newdir']") - self.assert_owner('/root/newdir', 'root:root') - - m.execute('useradd -m testuser') - b.go('/files#/?path=/home/testuser') - b.wait_not_present('.pf-v5-c-empty-state') - self.create_directory('newdir') - b.wait_visible("[data-item='newdir']") - self.assert_owner('/home/testuser', 'testuser:testuser') - - b.go('/files#/?path=/tmp') - b.wait_not_present('.pf-v5-c-empty-state') - self.addCleanup(m.execute, ['rm', '-rf', '/tmp/newdir']) - self.create_directory('newdir') - b.wait_visible("[data-item='newdir']") - self.assert_owner('/tmp/newdir', 'root:root') - - # Create as superuser but owned by admin:admin - self.addCleanup(m.execute, ['rm', '-rf', '/tmp/admindir']) - self.create_directory('admindir', 'admin:admin') - b.wait_visible("[data-item='admindir']") - self.assert_owner('/tmp/admindir', 'admin:admin') - - # Creating folder as user without administrator privileges - b.go('/files#/?path=/home/admin') - b.wait_not_present('.pf-v5-c-empty-state') - self.assert_last_breadcrumb("admin") - b.drop_superuser() - self.create_directory('admindir') - b.wait_visible("[data-item='admindir']") - self.assert_owner('/home/admin/admindir', 'admin:admin') - - # Pressing enter creates a directory - b.click("#dropdown-menu") - b.click("#create-folder") - # Keep focus on the input so the enter key press registers as form submit. - b.set_input_text("#create-directory-input", "testdir", blur=False) - b.key("Enter") - b.wait_not_present(".pf-v5-c-modal-box") - b.wait_visible("[data-item='testdir']") - - def testContextMenu(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - b.allow_download() - - # We should be able to click anywhere in this div. - body_size = b.eval_js(""" - [ - document.getElementById('files-card-parent').offsetWidth, - document.getElementById('files-card-parent').offsetHeight - ] - """) - - b.click("button[aria-label='Display as a list']") - - # Create folder from context menu, click in the middle to assert we can click everywhere. - b.mouse("#files-card-parent", "contextmenu", body_size[0] / 2, body_size[0] / 2) - b.click(".contextMenu button:contains('Create directory')") - b.set_input_text("#create-directory-input", "newdir") - b.click("button.pf-m-primary") - b.wait_visible("[data-item='newdir']") - - # Opening context menu from empty space deselects item - b.click("[data-item='newdir']") - b.mouse("#files-card-parent tbody", "contextmenu", body_size[1] / 4, body_size[1] / 4) - b.assert_pixels(".contextMenu", "overview-context-menu") - b.click(".contextMenu button:contains('Create directory')") - b.set_input_text("#create-directory-input", "newdir2") - b.click("button.pf-m-primary") - b.wait_visible("[data-item='newdir2']") - m.execute("rmdir /home/admin/newdir2") - - # Rename folder from context menu - b.mouse("[data-item='newdir']", "contextmenu") - b.assert_pixels(".contextMenu", "folder-context-menu") - b.click(".contextMenu button:contains('Rename')") - b.set_input_text("#rename-item-input", "newdir1") - b.click("button.pf-m-primary") - b.wait_visible("[data-item='newdir1']") - - # Edit permissions from context menu - m.execute("useradd testuser") - b.click("[data-item='newdir1']") - b.mouse("[data-item='newdir1']", "contextmenu") - b.click(".contextMenu button:contains('Edit permissions')") - b.select_from_dropdown("#edit-permissions-owner", "testuser") - b.click("button.pf-m-primary") - b.wait_in_text("[data-item='newdir1'] .item-owner", "testuser") - - if not m.ws_container and self.system_before(329): - # Open in terminal feature not supported before cockpit 329 - b.mouse("[data-item='newdir1']", "contextmenu") - b.wait_visible(".contextMenu button:contains('Delete')") - b.wait_not_present(".contextMenu button:contains('Open in terminal')") - b.mouse("#files-card-parent", "contextmenu", body_size[0] / 2, body_size[0] / 2) - b.wait_visible(".contextMenu button:contains('Create directory')") - b.wait_not_present(".contextMenu button:contains('Open in terminal')") - else: - # Open folder in terminal from context menu - b.mouse("[data-item='newdir1']", "contextmenu") - b.click(".contextMenu button:contains('Open in terminal')") - b.enter_page("/system/terminal") - b.switch_to_top() - b.wait_js_cond('String(window.location.pathname) === "/system/terminal"') - b.wait_js_cond('window.location.hash === "#/?path=%2Fhome%2Fadmin%2Fnewdir1"') - b.go("/files") - b.enter_page("/files") - - # Open current dir in terminal from context menu - b.mouse("#files-card-parent", "contextmenu", body_size[0] / 2, body_size[0] / 2) - b.click(".contextMenu button:contains('Open in terminal')") - b.enter_page("/system/terminal") - b.switch_to_top() - b.wait_js_cond('String(window.location.pathname) === "/system/terminal"') - b.wait_js_cond('window.location.hash === "#/?path=%2Fhome%2Fadmin%2F"') - b.go("/files") - b.enter_page("/files") - - # Regular files should not have terminal feature - m.execute("echo 'some content' > /home/admin/newfile") - b.mouse("[data-item='newfile']", "contextmenu") - b.wait_visible(".contextMenu button:contains('Delete')") - b.wait_not_present(".contextMenu button:contains('Open in terminal')") - - # Delete folder from context menu - b.mouse("[data-item='newdir1']", "contextmenu") - b.click(".contextMenu button:contains('Delete')") - b.click("button.pf-m-danger") - b.wait_not_present("[data-item='newdir1']") - - b.mouse("[data-item='newfile']", "contextmenu") - b.assert_pixels(".contextMenu", "file-context-menu") - b.click(".contextMenu button:contains('Download')") - size = int(self.stat('%s', '/home/admin/newfile')) - self.waitDownloadFile("newfile", size, "some content\n") - - # Delete button text should match item type: directory/file - b.mouse("[data-item='newfile']", "contextmenu") - b.click(".contextMenu button:contains('Delete')") - b.click("button.pf-m-danger") - b.wait_not_present("[data-item='newfile']") - - # The grid view also supports a contextmenu - m.execute("touch /home/admin/testfile") - b.click("button[aria-label='Display as a grid']") - b.mouse("[data-item='testfile']", "contextmenu") - b.wait_visible(".contextMenu button:contains('Delete')") - - def testDownload(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - b.allow_download() - - # Big file downloads fine - m.execute("truncate -s 1500M /home/admin/test.iso") - b.wait_visible("[data-item='test.iso']") - b.mouse("[data-item='test.iso']", "contextmenu") - b.click(".contextMenu button:contains('Download')") - self.waitDownloadFile("test.iso", 1500 * 1024 * 1024) - - # non-latin1 file is fine - m.execute("echo 'non-latin filename' > /home/admin/漢字") - b.wait_visible("[data-item='漢字']") - b.mouse("[data-item='漢字']", "contextmenu") - b.click(".contextMenu button:contains('Download')") - size = int(self.stat('%s', '/home/admin/漢字')) - self.waitDownloadFile("漢字", size, "non-latin filename\n") - m.execute("rm /home/admin/漢字") - - def testRename(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # validation - m.execute("touch /home/admin/newfile") - b.click("[data-item='newfile']") - b.click("#dropdown-menu") - b.click("#rename-item") - b.set_input_text("#rename-item-input", "test") - b.set_input_text("#rename-item-input", "") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#rename-item-input-helper", "Name cannot be empty") - b.assert_pixels(".pf-v5-c-modal-box", "rename-modal-error") - - b.set_input_text("#rename-item-input", "a" * 256) - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#rename-item-input-helper", "Name too long") - - b.set_input_text("#rename-item-input", "foo/bar") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_in_text("#rename-item-input-helper", "Name cannot include a /") - - b.set_input_text("#rename-item-input", "test") - b.wait_visible("button.pf-m-primary:not(:disabled)") - b.assert_pixels(".pf-v5-c-modal-box", "rename-modal") - b.click(".pf-v5-c-modal-box__footer button.pf-m-link") # cancel - - # Rename file - self.rename_item("newfile", "newfile1") - b.wait_visible("[data-item='newfile1']") - m.execute("rm /home/admin/newfile1") - - # Rename directory - m.execute("mkdir /home/admin/newdir") - self.rename_item("newdir", "newdir1") - b.wait_visible("[data-item='newdir1']") - - # Rename with space - self.rename_item("newdir1", "new dir1") - b.wait_visible("[data-item='new dir1']") - - # Rename to an existing directory should not move the file into the directory - m.execute(""" - touch /home/admin/newfile - mkdir /home/admin/dest - """) - b.wait_visible("[data-item='newfile']") - b.wait_visible("[data-item='dest']") - b.click("[data-item='newfile']") - b.click("#dropdown-menu") - b.click("#rename-item") - b.set_input_text("#rename-item-input", "dest") - b.wait_in_text("#rename-item-input-helper", "Directory with the same name exists") - b.click("button.pf-m-link:contains('Cancel')") - b.wait_not_present(".pf-v5-c-modal-box") - - # Renaming protected item should give an error - m.execute("chattr +i /home/admin/new\\ dir1") - self.addCleanup(m.execute, "chattr -i /home/admin/new\\ dir1") - self.rename_item("new dir1", "testdir") - alert_text = "mv: cannot move '/home/admin/new dir1' to '/home/admin/testdir': Operation not permitted" - self.wait_modal_inline_alert(alert_text) - b.click("div.pf-v5-c-modal-box__close > button") - - basedir = '/home/admin' - # Force overwrite rename on normal file - m.execute(f""" - echo 'foo text' > {basedir}/foo.txt - echo 'bar text' > {basedir}/bar.txt - mkdir {basedir}/foodir - mkdir {basedir}/bardir - """) - b.wait_visible("[data-item='foo.txt']") - b.wait_visible("[data-item='bar.txt']") - b.wait_visible("[data-item='foodir']") - b.wait_visible("[data-item='bardir']") - - # Overwrite regular file - self.file_action_modal('foo.txt', 'Rename') - b.set_input_text("#rename-item-input", "bar.txt") - b.wait_in_text("#rename-item-input-helper", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.click("button.pf-m-danger") - b.wait_not_present("[data-item='foo.txt']") - b.wait_visible("[data-item='bar.txt']") - contents = m.execute(f"cat {basedir}/bar.txt").strip() - self.assertEqual(contents, 'foo text') - - # Don't allow force overwrite on directory - self.file_action_modal('foodir', 'Rename') - b.set_input_text("#rename-item-input", "bardir") - b.wait_in_text("#rename-item-input-helper", "Directory with the same name exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - - # Trying to overwrite normal file to directory name shows error - self.file_action_modal('bar.txt', 'Rename') - b.set_input_text("#rename-item-input", "bardir") - b.wait_in_text("#rename-item-input-helper", "Directory with the same name exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - - # Overwriting symlinks is not supported - m.execute(f""" - echo 'foo text' > {basedir}/foo.txt - echo 'bar text' > {basedir}/bar.txt - ln -s {basedir}/foo.txt {basedir}/foolink.txt - ln -s {basedir}/bar.txt {basedir}/barlink.txt - """) - self.file_action_modal('foolink.txt', 'Rename') - b.set_input_text("#rename-item-input", "barlink.txt") - b.wait_in_text("#rename-item-input-helper", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - - # Trying to overwrite symlink to directory name shows error - m.execute(f""" - ln -s {basedir}/foodir {basedir}/foodirlink - """) - self.file_action_modal('foodirlink', 'Rename') - b.set_input_text("#rename-item-input", "bardir") - b.wait_in_text("#rename-item-input-helper", "Directory with the same name exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - - # Do not overwrite symlinks to directory with another file - self.file_action_modal('bardir', 'Rename') - b.set_input_text("#rename-item-input", "foodirlink") - b.wait_in_text("#rename-item-input-helper", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - self.file_action_modal('foo.txt', 'Rename') - b.set_input_text("#rename-item-input", "foodirlink") - b.wait_in_text("#rename-item-input-helper", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.wait_not_present("button.pf-m-danger") - b.click("div.pf-v5-c-modal-box__close > button") - - # Rename back to the original name - self.file_action_modal('foo.txt', 'Rename') - b.set_input_text("#rename-item-input", "bar.txt") - b.wait_in_text("#rename-item-input-helper", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.set_input_text("#rename-item-input", "foo.txt") - b.wait_in_text("#rename-item-input-helper", "Filename is the same as original name") - b.click("div.pf-v5-c-modal-box__close > button") - b.wait_not_present(".pf-v5-c-modal-box") - - # Enter allows renaming. - self.file_action_modal('foo.txt', 'Rename') - b.wait_val("#rename-item-input", 'foo.txt') - b.key('Enter') - b.wait_in_text("#rename-item-input-helper", "Filename is the same as original name") - b.set_input_text("#rename-item-input", "renamed-foo.txt", blur=False) - b.key('Enter') - b.wait_not_present(".pf-v5-c-modal-box") - b.wait_visible("[data-item='renamed-foo.txt']") - - # Rename file using keyboard shortcut - b.click("[data-item='newfile']") - b.key("F2") - b.wait_in_text(".pf-v5-c-modal-box__title-text", "Rename newfile") - b.set_input_text("#rename-item-input", "teddybear.txt") - b.click(".pf-v5-c-button.pf-m-primary") - b.wait_visible("[data-item='teddybear.txt']") - - # Rename modal does not open when multiple files are selected - b.mouse("[data-item='dest']", "click", ctrlKey=True) - b.mouse("[data-item='new dir1']", "click", ctrlKey=True) - b.key("F2") - b.wait_not_present(".pf-v5-c-modal-box__title-text") - - def testHiddenItems(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # Check hidden item count - m.execute("mkdir /home/admin/newdir") - m.execute("touch /home/admin/newdir/f1 /home/admin/newdir/.f2") - b.mouse("[data-item='newdir']", "dblclick") - b.wait_visible("[data-item='f1']") - b.wait_not_present("[data-item='.f2']") - b.wait_in_text(".files-footer-info", "Directory contains 0 directories, 1 files, 1 hidden") - - b.select_PF("#sort-menu-toggle", "Show hidden items") - b.wait_visible("[data-item='f1']") - b.wait_visible("[data-item='.f2']") - - # Selected option is saved in localStorage - b.reload() - b.enter_page("/files") - b.wait_visible("[data-item='f1']") - b.wait_visible("[data-item='.f2']") - - b.select_PF("#sort-menu-toggle", "Hide hidden items") - b.wait_visible("[data-item='f1']") - b.wait_not_present("[data-item='.f2']") - - def testPermissions(self) -> None: + def _testPermissions(self) -> None: b = self.browser m = self.machine has_selinux = not any(img in m.image for img in ["arch", "debian", "ubuntu", "suse"]) @@ -1795,1261 +744,6 @@ class TestFiles(testlib.MachineCase): for i in range(8): check_perms_match(f"file{i}", basedir) - def testErrors(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # Make a directory that's not readable to the admin user - m.execute("mkdir /home/admin/testdir && chmod 400 /home/admin/testdir") - b.mouse("[data-item='testdir']", "dblclick") - b.wait_not_present(".pf-v5-c-empty-state") - b.drop_superuser() - b.wait_in_text(".pf-v5-c-empty-state", "Permission denied") - b.assert_pixels(".pf-v5-c-page__main", "error-folder-view") - - # clicking on the home button should take us to the home directory - b.click("li[data-location='/home/admin'] a") - - # Now set a+r. We will be able to enter the directory now, and see the - # files present, but not read any information about them (not +x). - m.execute('touch /home/admin/testdir/testfile && chmod a+r /home/admin/testdir') - b.mouse("[data-item='testdir']", "dblclick") - self.assert_last_breadcrumb("testdir") - b.click("button[aria-label='Display as a list']") - b.wait_in_text("[data-item='testfile'] .item-owner", "") - b.wait_in_text("[data-item='testfile'] .item-date", "") - - def testMultiSelect(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # Check control-clicking - m.execute(""" - runuser -u admin touch /home/admin/file1 /home/admin/file2 - """) - - # Select all keybind - b.wait_visible("[data-item='file1']") - b.wait_visible("[data-item='file2']") - b.eval_js("window.focus()") - b.key("a", modifiers=["Control"]) - b.wait_visible("[data-item='file1'].row-selected") - b.wait_visible("[data-item='file2'].row-selected") - - b.click("[data-item='file1']") - b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].row-selected") - b.wait_visible("[data-item='file2'].row-selected") - b.assert_pixels("#files-card-parent", "multi-select-folder-view") - - b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].row-selected") - b.wait_not_present("[data-item='file2'].row-selected") - - b.mouse("[data-item='file1']", "click", ctrlKey=True) - b.wait_not_present("[data-item='file1'].row-selected") - - # Control-clicking when nothing is selected should select item normally - b.mouse("[data-item='file1']", "click", ctrlKey=True) - b.wait_visible("[data-item='file1'].row-selected") - - # Check context menu - b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.wait_visible("[data-item='file2'].row-selected") - b.mouse("[data-item='file1']", "contextmenu") - b.wait_in_text(".contextMenu li:nth-child(2) button", "Delete") - b.click(".contextMenu button:contains('Delete')") - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Delete 2 items?") - b.click("button.pf-m-danger") - b.wait_not_present("[data-item='file1']") - b.wait_not_present("[data-item='file2']") - - # Check sidebar menu - m.execute("touch /home/admin/file1 && touch /home/admin/file2") - b.click("[data-item='file1']") - b.mouse("[data-item='file2']", "click", ctrlKey=True) - b.click("#dropdown-menu") - b.click("#delete-item") - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Delete 2 items?") - b.click("button.pf-m-danger") - b.wait_not_present("[data-item='file1']") - b.wait_not_present("[data-item='file2']") - - def testKeyboardNav(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - m.execute(""" - runuser -u admin mkdir -p /home/admin/testdir - runuser -u admin mkdir -p /home/admin/anotherdir - """) - create_files = "" - for i in range(0, 4): - create_files += f"touch /home/admin/file{i}; " - m.execute(create_files) - - # view shortcuts help dialog - b.click("#dropdown-menu") - b.click("#shortcuts-help") - b.wait_in_text(".shortcuts-dialog .pf-v5-c-modal-box__title-text", "Keyboard shortcuts") - b.assert_pixels(".shortcuts-dialog", "shortcuts-help-menu") - b.click(".shortcuts-dialog button.pf-m-secondary") - b.wait_not_present(".shortcuts-dialog") - - # Focus the iframe for global keybindings in Files. - b.eval_js("window.focus()") - - # Pressing ArrowRight will select first item when nothing is selected - b.wait_visible(".pf-v5-c-table__tbody tr:nth-child(1)") - b.key("ArrowRight") - b.wait_visible(".pf-v5-c-table__tbody tr:nth-child(1).row-selected") - - b.click("[data-item='file0']") - b.wait_visible("[data-item='file0'].row-selected") - - b.key("ArrowRight") - b.wait_visible("[data-item='file1'].row-selected") - b.key("ArrowLeft") - b.wait_visible("[data-item='file0'].row-selected") - - # Go up and down in directory hierarchy - b.click("[data-item='testdir']") - b.key("ArrowDown", modifiers=["Alt"]) - self.assert_last_breadcrumb("testdir") - b.key("ArrowUp", modifiers=["Alt"]) - b.wait_visible("[data-item='testdir']") - - # Manually edit path - b.key("L", modifiers=["Control"]) - b.input_text("/home/admin/anotherdir") - b.key("Enter") - self.assert_last_breadcrumb("anotherdir") - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") - - b.key("N") - b.set_input_text("#create-directory-input", "foodir") - b.click("button.pf-m-primary") - b.wait_visible("[data-item='foodir']") - - # Up / Down depends on the layout, this is tested on mobile where the - # width is two or three columns. - b.set_layout("mobile") - b.click("[data-item='file0']") - b.wait_visible("[data-item='file0'].row-selected") - - b.key("ArrowDown") - b.wait_visible("[data-item='file2'].row-selected") - - b.key("ArrowUp") - b.wait_visible("[data-item='file0'].row-selected") - - # Test with very long hostnames - original_hostname = m.execute('hostname').strip() - self.addCleanup(m.execute, ['hostnamectl', 'set-hostname', original_hostname]) - # length of testing farm hostname - m.execute('hostnamectl set-hostname aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeee.testing-farm') - b.key("ArrowDown") - b.wait_visible("[data-item='file2'].row-selected") - - m.execute("mkdir /home/admin/foo") - b.click("[data-item='foo']") - - b.wait_visible("[data-item='foo'].row-selected") - b.key("Enter") - self.assert_last_breadcrumb("foo") - - def testCopyPaste(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - # Copy/paste file - m.execute("runuser -u admin mkdir /home/admin/newdir") - m.write('/home/admin/newfile', 'test_text\n', owner='admin:admin') - b.click("[data-item='newfile']") - b.click("#dropdown-menu") - b.click("#copy-item") - b.mouse("[data-item='newdir']", "dblclick") - self.assert_last_breadcrumb("newdir") - b.wait_in_text(".pf-v5-c-empty-state", "Directory is empty") - b.click("#dropdown-menu") - b.click("#paste-item") - b.wait_visible("[data-item='newfile']") - self.assertEqual(m.execute("head -n 1 /home/admin/newdir/newfile"), "test_text\n") - b.click("li[data-location='/home/admin'] a") - self.assert_last_breadcrumb("admin") - # original file still exists - b.wait_visible("[data-item='newfile']") - - # Copy/paste directory - m.execute("runuser -u admin mkdir /home/admin/copyDir") - m.execute("runuser -u admin mkdir /home/admin/newdir/loaded") - b.click("[data-item='copyDir']") - b.click("#dropdown-menu") - b.click("#copy-item") - b.mouse("[data-item='newdir']", "dblclick") - self.assert_last_breadcrumb("newdir") - b.wait_visible("[data-item='loaded']") - b.click("#dropdown-menu") - b.click("#paste-item") - b.wait_visible("[data-item='copyDir']") - b.click("li[data-location='/home/admin'] a") - self.assert_last_breadcrumb("admin") - b.wait_visible("[data-item='copyDir']") - - # File already exists error - m.write('/home/admin/newfile', 'changed', owner='admin:admin') - b.click("[data-item='newfile']") - b.click("#dropdown-menu") - b.click("#copy-item") - b.mouse("[data-item='newdir']", "dblclick") - b.click("#dropdown-menu") - b.click("#paste-item") - b.wait_visible("[data-item='newfile']") - self.assertEqual(m.execute("head -n 1 /home/admin/newdir/newfile"), "test_text\n") - b.wait_in_text("h4.pf-v5-c-alert__title", "Pasting failed") - b.wait_in_text(".pf-v5-c-alert__description", "\"newfile\" exists") - b.click("li[data-location='/home/admin'] a") - self.assert_last_breadcrumb("admin") - b.click(".pf-v5-c-alert__action button") - - # Copy/paste with keybinds - b.eval_js("window.focus()") - b.mouse("[data-item='newdir']", "dblclick") - b.click("[data-item='copyDir']") - b.key("c", modifiers=["Control"]) - m.execute("runuser -u admin mkdir -p /home/admin/kbdCopy") - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") - b.mouse("[data-item='kbdCopy']", "dblclick") - self.assert_last_breadcrumb("kbdCopy") - b.wait_text(".pf-v5-c-empty-state__title-text", "Directory is empty") - b.key("v", modifiers=["Control"]) - b.wait_visible("[data-item='copyDir']") - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") - m.execute("runuser -u admin echo 'keybindings good' > /home/admin/newdir/newfile") - b.mouse("[data-item='newdir']", "dblclick") - b.click("[data-item='loaded']") - b.mouse("[data-item='newfile']", "click", ctrlKey=True) - b.wait_visible("[data-item='loaded'].row-selected") - b.wait_visible("[data-item='newfile'].row-selected") - b.key("c", modifiers=["Control"]) - b.go("/files#/?path=/home/admin/kbdCopy") - self.assert_last_breadcrumb("kbdCopy") - b.wait_visible("[data-item='copyDir']") - m.execute("runuser -u admin rmdir /home/admin/kbdCopy/copyDir") - b.wait_not_present("[data-item='copyDir']") - b.key("v", modifiers=["Control"]) - b.wait_visible("[data-item='loaded']") - b.wait_visible("[data-item='newfile']") - self.assertEqual(m.execute("head -n 1 /home/admin/kbdCopy/newfile"), "keybindings good\n") - - # File already exists error with keybinds - b.go("/files#/?path=/home/admin") - self.assert_last_breadcrumb("admin") - m.execute("runuser -u admin echo 'changed' > /home/admin/newdir/newfile") - b.click("[data-item='newfile']") - b.key("c", modifiers=["Control"]) - b.go("/files#/?path=/home/admin/kbdCopy") - self.assert_last_breadcrumb("kbdCopy") - b.key("v", modifiers=["Control"]) - b.wait_in_text("h4.pf-v5-c-alert__title", "Pasting failed") - b.wait_in_text(".pf-v5-c-alert__description", "\"newfile\" exists") - self.assertEqual(m.execute("head -n 1 /home/admin/kbdCopy/newfile"), "keybindings good\n") - - @testlib.skipBrowser(".upload_files() doesn't work on Firefox", "firefox") - def testUpload(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - def get_piechart_progress(sel: str) -> int: - b.wait_visible(sel) - style = b.attr(sel, "style") - m = re.search(r"--progress: (\d+).\d+%;", style) - assert m is not None - return int(m.group(1)) - - def dir_file_count(directory: str) -> int: - return int(m.execute(f"find {directory} -mindepth 1 -maxdepth 1 | wc -l").strip()) - - def assert_upload_alert(files: List[str], owner_group: Optional[str] = None, *, - close: bool = True, pixel_test: bool = False) -> None: - title = "" - description = "" - if len(files) > 1: - title = "Files uploaded" - description = f"{len(files)} files" - else: - title = "File uploaded" - description = files[0] - - if owner_group: - # Default umask should be 022 - description += f"Uploaded as {owner_group}, rw- r-- r--" - - b.wait_in_text(".pf-v5-c-alert__title", title) - b.wait_text(".pf-v5-c-alert__description", description) - - if pixel_test: - b.assert_pixels(".pf-v5-c-alert.pf-m-success", "multiple-upload-alert-success") - - if close: - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - with tempfile.TemporaryDirectory() as tmpdir: - # Test cancelling of upload - big_file = str(Path(tmpdir) / "bigfile.img") - subprocess.check_call(["truncate", "-s", "1500MB", big_file]) - - m.execute("runuser -u admin mkdir /home/admin/Downloads") - b.wait_visible("[data-item='Downloads']") - b.mouse("[data-item='Downloads']", "dblclick") - b.wait_not_present(".pf-v5-c-empty-state") - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [big_file]) - b.wait_visible("#upload-file-btn:disabled") - - # Wait for some progress and cancel - b.click("#upload-progress-btn") - b.wait(lambda: get_piechart_progress("#upload-progress-btn") >= 2) - b.wait_in_text(".upload-progress-0", "bigfile.img") - b.assert_pixels(".upload-popover", "upload-popover", - ignore=[".upload-progress-0"], - skip_layouts=["mobile", "rtl"]) - b.wait(lambda: b.get_pf_progress_value(".upload-progress-0") >= 2) - - b.click(".cancel-button-0") - - b.wait_not_present("#upload-progress-btn") - b.wait_visible("#upload-file-btn") - b.wait_in_text(".pf-v5-c-alert__description", "Cancelled upload of bigfile.img") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - self.assertEqual(dir_file_count("/home/admin/Downloads"), 0) - - # Early ENOSPC error (before we would block on 'ack') - m.execute("mkdir -p /mnt/upload; mount -t tmpfs -o size=1M none /mnt/upload;") - # TODO: cockpit-bridge keeps a handle on /mnt/upload, pkill is a hack - self.addCleanup(m.execute, """ - pkill cockpit-bridge || true; - while mountpoint -q /mnt/upload && ! umount /mnt/upload; do sleep 0.2; done; - rmdir /mnt/upload; - """) - - b.go("/files#/?path=/mnt/upload") - self.assert_last_breadcrumb("upload") - b.wait_text(".pf-v5-c-empty-state__title-text", "Directory is empty") - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [big_file]) - - b.wait_in_text(".pf-v5-c-alert__description", "No space left on device") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - self.assertEqual(dir_file_count("/home/admin/Downloads"), 0) - - # Multi upload - dest_dir = "/home/admin/project" - m.execute(['runuser', '-u', 'admin', 'mkdir', dest_dir]) - b.go(f"/files#/?path={dest_dir}") - self.assert_last_breadcrumb("project") - b.wait_text(".pf-v5-c-empty-state__title-text", "Directory is empty") - - files = [str(Path(tmpdir) / f"{i}.txt") for i in range(0, 5)] - test_data = "this is a test" - for file in files: - with open(file, "w") as fp: - fp.write(test_data) - - def verify_uploaded_files(*, changed: bool = False) -> None: - for f in files: - contents = m.execute(f"cat {dest_dir}/{os.path.basename(f)}") - if changed: - self.assertNotEqual(contents, test_data) - else: - self.assertEqual(contents, test_data) - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [str(Path(tmpdir) / file) for file in files]) - with b.wait_timeout(30): - b.wait(lambda: int(m.execute(f"ls {dest_dir} | wc -l").strip()) == len(files)) - - assert_upload_alert(files, "admin:admin", pixel_test=True) - - # Verify ownership - filename = os.path.basename(files[0]) - b.click("button[aria-label='Display as a list']") - b.wait_in_text(f"[data-item='{filename}'] .item-owner", "admin") - b.click("button[aria-label='Display as a grid']") - - # Conflict handling - # change the content of the files locally so we can detect - # overwrites and make it noticeably bigger so we can assert that in - # the dialog details - for f in files: - with open(f, "w") as fp: - fp.write("new content" * 20) - - # Cancel does not overwrite - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [files[0]]) - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.assert_pixels(".pf-v5-c-modal-box.pf-m-warning", "upload-replace-dialog", - mock={".new-file-date": "Jun 19, 2024, 11:30 AM", - ".original-file-date": "Jun 19, 2024, 11:30 AM"}) - b.click("button.pf-m-link:contains('Cancel')") - # content did not change as we cancelled - self.assertEqual(m.execute(f"cat {dest_dir}/{filename}"), "this is a test") - - # Overwrite - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [files[0]]) - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.click("button.pf-m-warning:contains('Replace')") - b.wait_not_present(".pf-v5-c-modal-box") - assert_upload_alert([os.path.basename(files[0])], "admin:admin") - - self.assertEqual(m.execute(f"cat {dest_dir}/{filename}"), - subprocess.check_output(["cat", files[0]]).decode()) - # reset test file - m.execute(f"echo -n this is a test > {dest_dir}/{filename}") - - # Multiple files - - # Cancel all - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", files) - - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.wait_in_text(".pf-v5-c-check__label", "Apply this action to all conflicting files") - # Cancelling calls all - b.click("button.pf-m-link:contains('Cancel')") - b.wait_not_present(".pf-v5-c-modal-box") - - verify_uploaded_files() - - # Keep original for all - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", files) - - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.wait_in_text(".pf-v5-c-check__label", "Apply this action to all conflicting files") - b.set_checked("#replace-all", True) - b.click("button.pf-m-secondary:contains('Keep original')") - b.wait_not_present(".pf-v5-c-modal-box") - - verify_uploaded_files() - - # Replace all - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", files) - - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.wait_in_text(".pf-v5-c-check__label", "Apply this action to all conflicting files") - b.set_checked("#replace-all", True) - b.click("button.pf-m-warning:contains('Replace')") - b.wait_not_present(".pf-v5-c-modal-box") - - assert_upload_alert(files, "admin:admin") - verify_uploaded_files(changed=True) - - # Upload one file new file which is uploaded automatically - newfile = str(Path(tmpdir) / "newfile.txt") - with open(newfile, "w") as fp: - fp.write("bazinga") - - files.append(newfile) - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", files) - - # We only get asked about existing files - for file in files: - filename = os.path.basename(file) - if file != files[-1]: - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - b.click("button.pf-m-secondary:contains('Keep original')") - - b.wait_not_present(".pf-v5-c-modal-box") - b.wait_in_text(".pf-v5-c-alert__title", "File uploaded") - b.wait_in_text(".pf-v5-c-alert__description", "newfile.txt") - b.wait_in_text(".pf-v5-c-alert__description", "Uploaded as admin:admin") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - b.wait_visible(f"[data-item='{os.path.basename(newfile)}']") - - # Replace a the last file - - with open(newfile, "w") as fp: - fp.write("new content") - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", files) - - # We only get asked about existing files - for file in files: - filename = os.path.basename(file) - b.wait_in_text("h1.pf-v5-c-modal-box__title", f"Replace file {filename}?") - if file == files[-1]: - b.click("button.pf-m-warning:contains('Replace')") - else: - b.click("button.pf-m-secondary:contains('Keep original')") - - b.wait_not_present(".pf-v5-c-modal-box") - assert_upload_alert([os.path.basename(files[-1])], "admin:admin") - - self.assertEqual(m.execute(f"cat {dest_dir}/{filename}"), "new content") - - # As administrator upload in testuser and change ownership to root:root - - m.execute("useradd --create-home testuser") - b.go('/files#/?path=/home/testuser') - b.wait_visible('.pf-v5-c-empty-state') - - testuser_file = str(Path(tmpdir) / "testuser.txt") - test_content = "testdata" - with open(testuser_file, "w") as fp: - fp.write(test_content) - - testuser_filename = os.path.basename(testuser_file) - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [testuser_file]) - assert_upload_alert([testuser_filename], "testuser:testuser", close=False) - self.assert_owner(f'/home/testuser/{testuser_filename}', 'testuser:testuser') - contents = m.execute(f"cat /home/testuser/{testuser_filename}") - self.assertEqual(contents, test_content) - - b.click(".pf-v5-c-alert__action-group button") - b.wait_not_present(".pf-v5-c-alert__action") - b.wait_in_text(".pf-v5-c-modal-box__title-text", testuser_filename) - b.wait_val("#edit-permissions-owner", "testuser") - b.wait_val("#edit-permissions-group", "testuser") - b.wait_val("#edit-permissions-owner-access", "read-write") - b.wait_val("#edit-permissions-group-access", "read-only") - b.wait_val("#edit-permissions-other-access", "read-only") - - b.select_from_dropdown("#edit-permissions-owner", "root") - b.select_from_dropdown("#edit-permissions-group", "root") - b.click("button.pf-m-primary") - b.wait_not_present(".pf-v5-c-modal-box") - - self.assert_owner(f'/home/testuser/{testuser_filename}', 'root:root') - - # Uploading to an immutable dir fails to create a temp file, check that it is cleaned up - - m.execute("chattr +i /home/testuser") - self.addCleanup(m.execute, "chattr -i /home/testuser") - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [files[-1]]) - - b.go(f'/files#/?path={dest_dir}') - b.wait_not_present('.pf-v5-c-empty-state') - b.wait_in_text(".pf-v5-c-alert__title", "Failed") - b.wait_in_text(".pf-v5-c-alert__description", "Not permitted to perform this action") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - # Non-admin session - b.drop_superuser() - m.execute(f"rm {dest_dir}/{filename}; touch {dest_dir}/update.txt") - b.wait_not_present(f"[data-item='{filename}']") - # Wait for the update to appear so uploading doesn't flake - b.wait_visible("[data-item='update.txt']") - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [files[-1]]) - - b.wait_in_text(".pf-v5-c-alert__title", "File uploaded") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - b.wait_visible(f"[data-item='{filename}']") - - # Permission error - b.go("/files#/?path=/") - b.wait_visible("[data-item='bin']") - - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [files[0]]) - b.wait_in_text(".pf-v5-c-alert__description", "UploadError: Not permitted to perform this action") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - b.click("button[aria-label='Display as a list']") - b.go("/files#/?path=/home/admin") - - # Test drag & drop upload - b.eval_js(''' - function dragFileEvent(sel, eventType, filename) { - const el = window.ph_find(sel); - - let dataTransfer = null; - if (filename) { - // Synthetize a file DataTransfer action - const content = "Random file content: 4"; - const file = new File([content], filename, { type: "text/plain" }); - dataTransfer = new DataTransfer(); - dataTransfer.dropEffect = "move"; - - Object.defineProperty(dataTransfer, 'files', { - value: [file], - writable: false, - }); - } - - const ev = new DragEvent(eventType, { bubbles: true, dataTransfer: dataTransfer }); - el.dispatchEvent(ev); - }''') - - def drag_file_event(selector: str, dragType: str, filename: str | None = None) -> None: - b.wait_visible(selector) - b.call_js_func('dragFileEvent', selector, dragType, filename) - - b.wait_not_present(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - b.assert_pixels(".files-card", "drag-drop-upload-dropzone", - mock={".item-date": "Jun 19, 2024, 11:30 AM"}) - drag_file_event(".fileview-wrapper", 'drop', 'drag-drop-testfile.txt') - b.wait_in_text(".pf-v5-c-alert__title", "File uploaded") - b.click(".pf-v5-c-alert__action button") - b.wait_visible("[data-item='drag-drop-testfile.txt']") - b.wait_not_present(".pf-v5-c-alert__action") - b.wait_not_present(".drag-drop-upload") - - # Upload same file again - warning should show up - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragover') - drag_file_event(".fileview-wrapper", 'drop', 'drag-drop-testfile.txt') - b.wait_in_text("h1.pf-v5-c-modal-box__title", "Replace file drag-drop-testfile.txt?") - b.click(".pf-v5-c-modal-box button.pf-m-link") - b.wait_not_present(".pf-v5-c-modal-box") - - # Drag file around to test if upload icon shows up and hides accordingly - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragleave') - b.wait_not_present(".drag-drop-upload") - - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - # Hover over element which is a child of .fileview-wrapper - drag_file_event(".fileview-wrapper [data-item='Downloads'] .item-name", 'dragenter') - drag_file_event(".fileview-wrapper", 'dragleave') - b.wait_visible(".drag-drop-upload") - drag_file_event(".fileview-wrapper [data-item='project'] .item-name", 'dragenter') - drag_file_event(".fileview-wrapper [data-item='Downloads'] .item-name", 'dragleave') - b.wait_visible(".drag-drop-upload") - # Drag away from the .fileview-wrapper - drag_file_event(".fileview-wrapper [data-item='project'] .item-name", 'dragleave') - b.wait_not_present(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragleave') - b.wait_not_present(".drag-drop-upload") - - # Drag & drop is disabled when upload is in progress - b.click("#upload-file-btn") - b.upload_files("#upload-file-btn + input[type='file']", [big_file]) - b.wait_visible("#upload-file-btn:disabled") - b.wait_not_present(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload-blocked") - b.assert_pixels(".files-card", "drag-drop-upload-dropzone-blocked", - mock={".item-date": "Jun 19, 2024, 11:30 AM"}) - drag_file_event(".fileview-wrapper", 'dragleave') - b.wait_not_present(".drag-drop-upload-blocked") - # Dropping the file does nothing - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload-blocked") - drag_file_event(".fileview-wrapper", 'drop', 'drag-drop-file-2.txt') - b.wait_not_present(".drag-drop-upload") - b.click("#upload-progress-btn") - b.wait_in_text(".upload-progress-0", "bigfile.img") - b.wait_not_present("[data-item='drag-drop-file-2.txt']") - b.click("#upload-progress-btn") - b.click(".cancel-button-0") - b.click(".pf-v5-c-alert__action button") - - # Change directory and upload - b.mouse("[data-item='project']", "dblclick") - drag_file_event(".fileview-wrapper", 'dragenter') - b.wait_visible(".drag-drop-upload") - drag_file_event(".fileview-wrapper", 'drop', 'drag-drop-testfile.txt') - b.wait_in_text(".pf-v5-c-alert__title", "File uploaded") - b.click(".pf-v5-c-alert__action button") - b.wait_visible("[data-item='drag-drop-testfile.txt']") - b.wait_not_present(".pf-v5-c-alert__action") - b.wait_not_present(".drag-drop-upload") - - def testFileTypes(self) -> None: - m = self.machine - b = self.browser - - exts = { - '': 'file', - '.tar.gz': 'archive-file', - '.ogg': 'audio-file', - '.py': 'code-file', - '.png': 'image-file', - '.txt': 'text-file', - '.mkv': 'video-file', - } - - files = m.execute(fr""" - cd ~admin - - # for each different file category, as per extension recognition... - for ext in {shlex.join(exts)}; do - # ... create one of each of the 7 fundamental types, minus symlinks - mkdir "dir$ext" - truncate -s1234 "file$ext" - mkfifo "fifo$ext" - python3 -c "import socket; socket.socket(socket.AF_UNIX).bind('sock$ext')" - mknod "chrdev$ext" c 0 0 - mknod "blkdev$ext" b 0 0 - - # different types of broken symlink - ln -sf "loop$ext" "loop$ext" - ln -sf /bzzt "broken$ext" - done - - # create some symlinks to those things - for source in *; do - ln -sf "$source" sym-"$source" - ln -sf "sym-$source" sym-sym-"$source" - done - - ls /home/admin # get the result - """).split() - - # Make sure we created what we expected: - # - len(exts) extension types; times - # - 6 fundamental types plus 2 broken symlinks; times - # - 3 levels of additional symlinking ('file', 'sym-file', 'sym-sym-file') - self.assertEqual(len(files), len(exts) * (6 + 2) * 3) - - self.login_and_go("/files") - b.click("button[aria-label='Display as a list']") - - # For each file we created, assert various things about how we expect - # it to be displayed. - for name in files: - selector = f"[data-item='{name}']" - classes = b.attr(selector, 'class').split() - self.assertEqual(b.text(f'{selector} .item-name'), name) - size = b.text(f'{selector} [data-label="size"]') - date = b.text(f'{selector} [data-label="date"]') - - if 'dir' in name: - # directories are shown with folder icon, regardless of extension - self.assertIn('folder', classes) - elif 'file' in name: - _, dot, ext = name.partition('.') - self.assertIn(exts[dot + ext], classes) - else: - # specials are shown with file icon (for now), regardless of extension - # that includes broken symlinks ('loop*', 'broken*') - self.assertIn('file', classes) - - # check if the symlink icon is present/missing, as appropriate - if name.startswith(('sym', 'loop', 'broken')): - self.assertIn('symlink', classes) - else: - self.assertNotIn('symlink', classes) - - # check the size field — it should only be present directly on - # files and not on symlinks to files (like 'sym-file.txt', etc) and - # definitely not shown for any other file type - if name.startswith('file'): - self.assertEqual(size, '1.23 kB') - else: - self.assertEqual(size, '') - - # we should always have a reasonable date — make sure it parses - datetime.datetime.strptime(date + ' +0000', '%b %d, %Y, %I:%M %p %z') - - # Make sure nothing else was present - b.wait_js_cond(f"ph_count('#folder-view tbody tr') == {len(files)}") - b.assert_pixels("#files-card-parent", "icon-list-view", mock={".item-date": "Jun 19, 2024, 11:30 AM"}) - - def testBookmark(self) -> None: - b = self.browser - m = self.machine - config_file = "/home/admin/.config/gtk-3.0/bookmarks" - - def read_config() -> str: - return m.execute(f"cat {config_file}") - - self.enter_files() - - def assert_bookmark(bookmark: str, *, exists: bool = True) -> None: - b.click("#bookmark-btn") - b.wait_visible(".pf-v5-c-menu") - if exists: - b.wait_visible(f".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('{bookmark}') button") - else: - # Has to wait on file.watch() (inotify) to re-read the bookmarks - b.wait_not_in_text(".pf-v5-c-menu", bookmark) - - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item") - - b.click("#bookmark-btn") - # There is only one menu item - b.wait_in_text(".pf-v5-c-menu .pf-v5-c-menu__list-item", "Home") - # Home directory cannot be added or removed as bookmark - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark)") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Remove current directory)") - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item") - - # Add a bookmark - b.go("/files#/?path=/etc") - self.assert_last_breadcrumb("etc") - b.wait_visible("[data-item='passwd']") - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - b.wait_not_present(".pf-v5-c-menu") - - assert_bookmark("/etc", exists=True) - - # Remove bookmark - b.click("#bookmark-btn") - b.wait_visible(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('/etc') button") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Remove current directory') button") - b.wait_not_present(".pf-v5-c-menu") - - assert_bookmark("/etc", exists=False) - - # Go to bookmark - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('/etc') button") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - b.wait_not_present(".pf-v5-c-menu") - - b.go("/files#/?path=/proc") - b.wait_not_present(".pf-v5-c-empty-state") - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('/etc') button") - b.wait_not_present(".pf-v5-c-empty-state") - self.assert_last_breadcrumb("etc") - - # Bookmarks from nautilus are supported - m.execute("runuser -u admin mkdir '/home/admin/This is a-test'") - m.write(config_file, - "file:///home/admin/This%20is%20a%2dtest\n" - "file:///tmp Temporary Directory\n", - append=True, owner='admin:admin') - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('This is a-test') button") - self.assert_last_breadcrumb("This is a-test") - - # Removing directory with spaces or aliases - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Remove current directory') button") - b.wait_not_present(".pf-v5-c-menu") - - assert_bookmark("This is a-test", exists=False) - - # Bookmarking a directory with spaces encodes it correctly - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - - assert_bookmark("This is a-test", exists=True) - self.assertIn("file:///home/admin/This%20is%20a-test/\n", read_config()) - - # Removing /tmp bookmark keeps bookmark with spaces - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('tmp') button") - self.assert_last_breadcrumb("tmp") - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Remove current directory') button") - b.wait_not_present(".pf-v5-c-menu") - - assert_bookmark("/tmp", exists=False) - self.assertEqual(read_config(), "file:///etc/\nfile:///home/admin/This%20is%20a-test/\n") - - # Add a directory with special characters - special_dir = "super#special*1 2><.,ß" - m.execute(f"runuser -u admin mkdir '/home/admin/{special_dir}'") - b.go("/files#/?path=/home/admin") - b.wait_visible(f"[data-item='{special_dir}'") - b.mouse(f"[data-item='{special_dir}']", "dblclick") - self.assert_last_breadcrumb(special_dir) - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - - assert_bookmark(special_dir, exists=True) - self.assertEqual("file:///etc/\nfile:///home/admin/This%20is%20a-test/\nfile:///home/admin/super%23special*1%202%3E%3C.%2C%C3%9F/\n", - read_config()) - - b.click("li[data-location='/home/admin'] a") - self.assert_last_breadcrumb("admin") - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('super#special') button") - self.assert_last_breadcrumb(special_dir) - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Remove current directory') button") - b.wait_not_present(".pf-v5-c-menu") - - assert_bookmark(special_dir, exists=False) - self.assertEqual(read_config(), "file:///etc/\nfile:///home/admin/This%20is%20a-test/\n") - - # Modifications outside of Cockpit are shown - m.write(config_file, - "nfs://lalala\n" - "file:///var\n", - append=True, owner='admin:admin') - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('/var') button") - b.wait_not_present(".pf-v5-c-empty-state") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('lalala') button") - self.assert_last_breadcrumb("var") - - # Removing bookmarks file removes all bookmarks - m.execute(['rm', '-rf', config_file]) - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('/etc') button") - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu") - - # Remove a bookmark when the directory is removed - m.write(config_file, - "file:///tmp/non-existent\n", - owner='admin:admin') - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('non-existent') button") - self.assert_last_breadcrumb("non-existent") - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Remove current directory') button") - b.wait_not_present(".pf-v5-c-menu") - # Removing the last bookmark, empties the file - m.execute(f"until [ $(stat -c '%s' {config_file}) -eq 0 ]; do sleep 1; done") - - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains('Add bookmark') button") - b.click("#bookmark-btn") - b.wait_not_present(".pf-v5-c-menu") - - # Error conditions - - # When we can't create the directory - m.execute(""" - rm -fr /home/admin/.config/gtk-3.0 - chattr +i /home/admin/.config - """) - - b.go("/files#/?path=/var") - self.assert_last_breadcrumb("var") - - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - self.wait_modal_inline_alert("Unable to create bookmark directory") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - m.execute("chattr -i /home/admin/.config") - - # When directory is not writable for us - m.execute("mkdir /home/admin/.config/gtk-3.0") - b.click("#bookmark-btn") - b.click(".pf-v5-c-menu .pf-v5-c-menu__list-item:contains(Add bookmark) button") - self.wait_modal_inline_alert("Unable to save bookmark file") - b.click(".pf-v5-c-alert__action button") - b.wait_not_present(".pf-v5-c-alert__action") - - def testEditor(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - def open_editor(file: str) -> None: - b.mouse(f"[data-item='{file}']", "contextmenu") - b.click(".contextMenu button:contains('Open text file')") - - def validate_content(file: str, content: str) -> None: - open_editor(file) - b.wait_val(".file-editor-modal textarea", content) - b.click(".file-editor-modal button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - # validate on disk - self.assertEqual(m.execute(f"cat /home/admin/{file}"), content) - - m.execute(""" - runuser -u admin -- bash -c "echo test > /home/admin/test.txt" - runuser -u admin -- bash -c "echo planes > /home/admin/notes.txt" - runuser -u admin -- truncate -s 2M /home/admin/big.txt - runuser -u admin -- touch /home/admin/archive.tar.gz - """) - - # Don't allow opening big text files - b.mouse("[data-item='big.txt']", "contextmenu") - b.wait_not_in_text(".contextMenu", "Open text file") - b.click("#files-card-parent tbody") - b.wait_not_present(".contextMenu") - - # A tarball cannot be edited - b.mouse("[data-item='archive.tar.gz']", "contextmenu") - b.wait_not_in_text(".contextMenu", "Open text file") - b.click("#files-card-parent tbody") - b.wait_not_present(".contextMenu") - - # Opening an test file, editing and closing (discard) - open_editor("test.txt") - b.wait_text(".pf-v5-c-modal-box__title", "Edit test.txt") - b.wait_text(".file-editor-modal textarea", "test\n") - - # Save button is disabled when not editing yet - b.wait_visible(".pf-v5-c-modal-box__footer .pf-m-primary.pf-m-disabled") - b.set_input_text(".file-editor-modal textarea", "foobar", append=True, value_check=False) - b.wait_val(".file-editor-modal textarea", "test\nfoobar") - - b.assert_pixels(".file-editor-modal", "editor-modal-changed") - b.wait_visible(".pf-v5-c-modal-box__footer .pf-m-primary:not(:disabled)") - - b.click(".file-editor-modal button.pf-m-link") - b.wait_not_present(".file-editor-modal") - self.assertEqual(m.execute("cat /home/admin/test.txt"), "test\n") - - self.assert_owner('/home/admin/test.txt', 'admin:admin') - - # Opening a test file, add text and save - open_editor("test.txt") - b.set_input_text(".file-editor-modal textarea", "foobar", append=True, value_check=False) - b.wait_val(".file-editor-modal textarea", "test\nfoobar") - b.wait_visible(".file-editor-modal.is-modified") - b.click(".file-editor-modal button.pf-m-primary") - # Saving resets modified state so should not be shown - b.wait_not_present(".file-editor-modal.is-modified") - - b.click(".file-editor-modal button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - validate_content("test.txt", "test\nfoobar\n") - self.assert_owner('/home/admin/test.txt', 'admin:admin') - - # Opening a test file, edit text and someone else changes it while we are editing - open_editor("test.txt") - b.set_input_text(".file-editor-modal textarea", "roll\n", append=True, value_check=False) - b.wait_val(".file-editor-modal textarea", "test\nfoobar\nroll\n") - b.wait_visible(".pf-v5-c-modal-box__footer .pf-m-primary:not(:disabled)") - - m.execute("runuser -u admin echo 'testing' > /home/admin/test.txt") - b.wait_in_text(".pf-v5-c-alert__title", "The file has changed on disk") - - # Abort, reload - b.click(".file-editor-modal button:contains('Reload')") - b.wait_val(".file-editor-modal textarea", "testing\n") - - # Let someone else edit again and overwrite - b.set_input_text(".file-editor-modal textarea", "roll\n", append=True, value_check=False) - b.wait_val(".file-editor-modal textarea", "testing\nroll\n") - m.execute("echo 'testing' > /home/admin/test.txt") - b.wait_in_text(".pf-v5-c-alert__title", "The file has changed on disk") - - b.click(".pf-v5-c-modal-box__footer .pf-m-warning") - b.wait_not_present(".pf-v5-c-alert__title") - b.click(".file-editor-modal button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - validate_content("test.txt", "testing\nroll\n") - self.assert_owner('/home/admin/test.txt', 'admin:admin') - - # Change ownership during editing - open_editor("test.txt") - b.wait_val(".file-editor-modal textarea", "testing\nroll\n") - m.execute("chown root: /home/admin/test.txt") - b.set_input_text(".file-editor-modal textarea", "reset") - b.click(".pf-v5-c-modal-box__footer .pf-m-primary") - b.wait_in_text(".pf-v5-c-alert.pf-m-warning", "The file has changed on disk") - - b.click(".pf-v5-c-modal-box__footer .pf-v5-c-button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - # Emptying a file does not remove the file - open_editor("notes.txt") - b.wait_val(".file-editor-modal textarea", "planes\n") - b.set_input_text(".file-editor-modal textarea", "") - b.click(".pf-v5-c-modal-box__footer .pf-m-primary") - b.wait_visible(".pf-v5-c-modal-box__footer .pf-m-primary:disabled") - - b.click(".pf-v5-c-modal-box__footer .pf-v5-c-button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - validate_content("notes.txt", "") - - # Remove file during editing - open_editor("notes.txt") - b.wait_val(".file-editor-modal textarea", "") - b.set_input_text(".file-editor-modal textarea", "reset") - m.execute("rm /home/admin/notes.txt") - b.click(".pf-v5-c-modal-box__footer .pf-m-primary") - b.wait_in_text(".pf-v5-c-alert.pf-m-warning", "The file has been removed on disk") - - # Save, file was gone but we write it again - b.click(".pf-v5-c-modal-box__footer .pf-v5-c-button.pf-m-primary") - # Saving resets modified state so should not be shown - b.wait_not_present(".file-editor-modal.is-modified") - - b.click(".pf-v5-c-modal-box__footer .pf-v5-c-button.pf-m-link") - b.wait_not_present(".file-editor-modal") - - validate_content("notes.txt", "reset\n") - - # Escape closes the dialog - open_editor("notes.txt") - b.blur(".file-editor-modal textarea") # remove focus from textarea - b.key("Escape") # Escape for modal - b.wait_not_present(".file-editor-modal") - - # As unprivileged user - b.drop_superuser() - b.wait_not_present('.pf-v5-c-empty-state') - self.assert_last_breadcrumb("admin") - - # View a file - m.execute("echo 'this is a config file' > /etc/cockpit-files-test.cfg") - self.addCleanup(m.execute, "rm /etc/cockpit-files-test.cfg") - b.go("/files#/?path=/etc") - b.wait_not_present('.pf-v5-c-empty-state') - self.assert_last_breadcrumb("etc") - open_editor("cockpit-files-test.cfg") - b.wait_text(".pf-v5-c-modal-box__title", "View cockpit-files-test.cfgRead-only") - b.assert_pixels(".file-editor-modal", "editor-modal-read-only") - - b.click(".pf-v5-c-modal-box__footer button.pf-m-secondary") - b.wait_not_present(".file-editor-modal") - - def testCreateFile(self) -> None: - b = self.browser - m = self.machine - - self.enter_files() - - def open_create_file_modal() -> None: - b.mouse("#files-card-parent", "contextmenu") - b.click(".contextMenu button:contains('Create file')") - b.wait_visible(".file-create-modal") - b.wait_text(".pf-v5-c-modal-box__title-text", "Create file") - b.wait_visible("button.pf-m-primary:disabled") - - # Error cases - # cancel does not create a file - open_create_file_modal() - b.set_input_text("#file-name", "test.txt") - b.set_input_text(".file-create-modal textarea", "content") - b.wait_visible("button.pf-m-primary:not(:disabled)") - b.click(".pf-v5-c-modal-box__footer button.pf-m-link") # cancel - b.wait_not_present(".pf-v5-c-modal-box") - m.execute("! test -f test.txt") - - # file already exists - m.execute("runuser -u admin touch /home/admin/test.txt") - b.wait_visible("[data-item='test.txt']") - open_create_file_modal() - b.set_input_text("#file-name", "test.txt") - b.wait_text(".file-create-modal .pf-v5-c-helper-text__item-text", "File exists") - b.wait_visible("button.pf-m-primary:disabled") - b.set_input_text("#file-name", "/test.txt") - b.wait_text(".file-create-modal .pf-v5-c-helper-text__item-text", "Name cannot include a /") - b.wait_visible("button.pf-m-primary:disabled") - - # Create a file as admin, we are superuser and /home owned by admin - b.set_input_text("#file-name", "notes.txt") - b.set_input_text(".file-create-modal textarea", "content") - b.wait_val("#create-file-owner", "admin:admin") - b.assert_pixels(".pf-v5-c-modal-box", "create-file-modal-superuser") - b.click("button.pf-m-primary") - b.wait_not_present(".pf-v5-c-modal-box") - self.assert_owner("/home/admin/notes.txt", "admin:admin") - self.assertEqual(m.execute("cat /home/admin/notes.txt"), "content") - - # create a file as root. - open_create_file_modal() - b.set_input_text("#file-name", "root.txt") - b.set_input_text(".file-create-modal textarea", "root") - b.select_from_dropdown("#create-file-owner", "root:root") - b.click("button.pf-m-primary") - b.wait_not_present(".pf-v5-c-modal-box") - self.assert_owner("/home/admin/root.txt", "root:root") - self.assertEqual(m.execute("cat /home/admin/root.txt"), "root") - - # normal user has no change owner select - b.drop_superuser() - - open_create_file_modal() - b.wait_not_present("#create-file-owner") - b.set_input_text("#file-name", "admin.txt") - b.set_input_text(".file-create-modal textarea", "admin") - b.assert_pixels(".pf-v5-c-modal-box", "create-file-modal-admin") - b.click("button.pf-m-primary") - b.wait_not_present(".pf-v5-c-modal-box") - self.assert_owner("/home/admin/admin.txt", "admin:admin") - self.assertEqual(m.execute("cat /home/admin/admin.txt"), "admin") - - # Escape closes the dialog - self.open_folder_context_menu() - b.click(".contextMenu button:contains('Create file')") - b.blur("#file-name") # remove focus from input - b.key("Escape") # Escape for modal - b.wait_not_present(".pf-v5-c-modal-box") - - # Escape does not close the the dialog when a filename is entered - self.open_folder_context_menu() - b.click(".contextMenu button:contains('Create file')") - b.set_input_text("#file-name", "admin.txt") - b.blur("#file-name") # remove focus from input - b.key("Escape") # Escape for modal - b.wait_val("#file-name", "admin.txt") - b.click(".pf-v5-c-modal-box__footer button.pf-m-link") # cancel - b.wait_not_present(".pf-v5-c-modal-box") - - # Unable to create a file - self.open_folder_context_menu() - b.click(".contextMenu button:contains('Create file')") - b.set_input_text("#file-name", "notallowed.txt") - b.set_input_text(".file-create-modal textarea", "nope") - m.execute("chattr +i /home/admin") - self.addCleanup(m.execute, "chattr -i /home/admin") - b.click("button.pf-m-primary") - self.wait_modal_inline_alert("Not permitted to perform this action") - b.click(".pf-v5-c-modal-box__footer button.pf-m-link") # cancel - b.wait_not_present(".pf-v5-c-modal-box") - - # create a file as admin - - # cannot create file in /home as normal user - b.click("li[data-location='/home'] a") - self.assert_last_breadcrumb("home") - self.open_folder_context_menu() - b.click(".contextMenu button:contains('Create file')") - b.wait_in_text("h4.pf-v5-c-alert__title", "Cannot create file in current directory") - if __name__ == '__main__': testlib.test_main() diff --git a/test/run b/test/run index 2e503928..7f8e44e6 100755 --- a/test/run +++ b/test/run @@ -12,6 +12,5 @@ if [ "$TEST_SCENARIO" = "devel" ]; then export TEST_COVERAGE=yes fi -make codecheck make check make po/files.pot