324 lines
12 KiB
Python
Executable File
324 lines
12 KiB
Python
Executable File
#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../common/pywrap", sys.argv)
|
|
|
|
# This file is part of Cockpit.
|
|
#
|
|
# Copyright (C) 2017 Red Hat, Inc.
|
|
#
|
|
# Cockpit is free software; you can redistribute it and/or modify it
|
|
# under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation; either version 2.1 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Cockpit is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
from packagelib import PackageCase
|
|
from testlib import nondestructive, skipImage, skipOstree, test_main
|
|
|
|
|
|
@skipImage("TODO: bug in refreshing removed/installed state on Arch Linux", "arch")
|
|
@skipOstree("Not supported")
|
|
@nondestructive
|
|
class TestApps(PackageCase):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.appstream_collection = set()
|
|
self.machine.upload(["verify/files/test.png"], "/var/tmp/")
|
|
|
|
def createAppStreamPackage(self, name, version, revision):
|
|
self.createPackage(name, version, revision, content={
|
|
f"/usr/share/metainfo/org.cockpit-project.{name}.metainfo.xml": f"""
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.{name}</id>
|
|
<icon type="local">/usr/share/pixmaps/test.png</icon>
|
|
<name>{name}</name>
|
|
<summary>An application for testing</summary>
|
|
<launchable type="cockpit-manifest">{name}</launchable>
|
|
</component>
|
|
""",
|
|
"/usr/share/pixmaps/test.png": {"path": "/var/tmp/test.png"}})
|
|
self.appstream_collection.add(name)
|
|
|
|
def createAppStreamRepoPackage(self):
|
|
body = ""
|
|
for p in self.appstream_collection:
|
|
body += f"""
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.{p}</id>
|
|
<icon type="cached">test.png</icon>
|
|
<name>{p}</name>
|
|
<summary>An application for testing</summary>
|
|
<launchable type="cockpit-manifest">{p}</launchable>
|
|
<description>
|
|
<p>DESCRIPTION:none</p>
|
|
</description>
|
|
<url type="homepage">https://{p}.com</url>
|
|
<pkgname>{p}</pkgname>
|
|
</component>
|
|
"""
|
|
self.machine.execute("mkdir -p /usr/share/app-info/xmls")
|
|
self.createPackage("appstream-data-test", "1.0", "1", content={
|
|
"/usr/share/app-info/xmls/test.xml": f"""
|
|
<components origin="test">
|
|
{body}
|
|
</components>
|
|
""",
|
|
"/usr/share/app-info/icons/test/64x64/test.png": {"path": "/var/tmp/test.png"}})
|
|
self.enableRepo()
|
|
self.machine.execute("systemctl stop packagekit; pkcon refresh force")
|
|
# ignore the corresponding journal entry
|
|
self.allow_journal_messages("org.freedesktop.PackageKit.*org.freedesktop.DBus.Error.NoReply.*")
|
|
|
|
def testBasic(self, urlroot=""):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
self.allow_journal_messages("can't remove watch: Invalid argument")
|
|
|
|
self.restore_dir("/usr/share/metainfo", reboot_safe=True)
|
|
self.restore_dir("/usr/share/app-info", reboot_safe=True)
|
|
self.restore_dir("/var/cache/app-info", reboot_safe=True)
|
|
|
|
# Make sure none of the appstream directories exist. They
|
|
# will be created later and we need to cope with that.
|
|
m.execute("rm -rf /usr/share/metainfo /usr/share/app-info /var/cache/app-info")
|
|
|
|
# instead of the actual distro packages, use our own fake repo data package
|
|
self.write_file("/etc/cockpit/apps.override.json",
|
|
'{ "config": { "appstream_data_packages": [ "appstream-data-test" ] } }')
|
|
|
|
if urlroot != "":
|
|
m.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")
|
|
|
|
self.login_and_go("/apps", urlroot=urlroot)
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
|
|
self.createAppStreamPackage("app-1", "1.0", "1")
|
|
self.createAppStreamRepoPackage()
|
|
|
|
# Refresh package info
|
|
b.click("#refresh")
|
|
|
|
with b.wait_timeout(30):
|
|
b.click(".app-list #app-1")
|
|
b.wait_visible('a[href="https://app-1.com"]')
|
|
b.wait_visible(f'#app-page img[src^="{urlroot}/cockpit/channel/"]')
|
|
b.click(".pf-v5-c-breadcrumb a:contains('Applications')")
|
|
|
|
b.wait_visible("#list-page")
|
|
b.wait_not_present("#app-page")
|
|
|
|
b.click(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Install')")
|
|
b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Remove')")
|
|
b.wait_visible(f".app-list .pf-v5-c-data-list__item-row:contains('app-1') img[src^='{urlroot}/cockpit/channel/']")
|
|
m.execute("test -f /stamp-app-1-1.0-1")
|
|
|
|
b.click(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Remove')")
|
|
b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('app-1') button:contains('Install')")
|
|
b.wait_visible(f".app-list .pf-v5-c-data-list__item-row:contains('app-1') img[src^='{urlroot}/cockpit/channel/']")
|
|
m.execute("! test -f /stamp-app-1-1.0-1")
|
|
|
|
def testWithUrlRoot(self):
|
|
self.testBasic(urlroot="/webcon")
|
|
|
|
def testL10N(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# Switching to a language might produce these messages, which seem to be harmless.
|
|
self.allow_journal_messages("invalid or unusable locale.*",
|
|
"Error .* data: Connection reset by peer")
|
|
|
|
# Reset everything
|
|
m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
|
|
self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do umount $d; done")
|
|
|
|
self.login_and_go("/apps")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components origin="test">
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.foo</id>
|
|
<name>NAME:none</name>
|
|
<name xml:lang="de">NAME:de</name>
|
|
<summary>SUMMARY:none</summary>
|
|
<summary xml:lang="de">SUMMARY:de</summary>
|
|
<description>
|
|
<p>DESCRIPTION:none</p>
|
|
<p xml:lang="de">DESCRIPTION:de</p>
|
|
</description>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
</components>""")
|
|
|
|
b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('SUMMARY:none') button:contains('Install')")
|
|
b.click(".app-list .pf-v5-c-data-list__item-row:contains('SUMMARY:none') .pf-m-inline")
|
|
|
|
b.wait_visible(".app-description:contains('DESCRIPTION:none')")
|
|
|
|
def set_lang(lang):
|
|
b.switch_to_top()
|
|
b.open_session_menu()
|
|
b.click(".display-language-menu")
|
|
b.wait_visible('#display-language-modal')
|
|
if self.system_before(242): # Changed in #15667
|
|
b.set_val("#display-language-modal select", lang)
|
|
else:
|
|
b.click(f'#display-language-modal li[data-value={lang}] button')
|
|
b.click("#display-language-modal footer button.pf-m-primary")
|
|
b.wait_language(lang)
|
|
b.enter_page("/apps")
|
|
|
|
set_lang("de-de")
|
|
b.wait_visible(".app-description:contains('DESCRIPTION:de')")
|
|
b.wait_not_present(".app-description:contains('DESCRIPTION:none')")
|
|
|
|
set_lang("ja-jp")
|
|
b.wait_visible(".app-description:contains('DESCRIPTION:none')")
|
|
b.wait_not_present(".app-description:contains('DESCRIPTION:de')")
|
|
|
|
# like in the general whitelist, but translated
|
|
self.allow_journal_messages("xargs: basename: .*Signal 13.*")
|
|
|
|
def testBrokenXML(self):
|
|
b = self.browser
|
|
m = self.machine
|
|
|
|
# Reset everything
|
|
m.execute("for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do mkdir -p $d; mount -t tmpfs tmpfs $d; done")
|
|
self.addCleanup(m.execute, "for d in /usr/share/metainfo /usr/share/app-info /var/cache/app-info; do umount $d; done")
|
|
|
|
self.login_and_go("/apps")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
|
|
self.allow_journal_messages(".*/usr/share/app-info/xmls/test.xml.*",
|
|
".*xml.etree.ElementTree.ParseError.*")
|
|
|
|
def reset():
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components origin="test">
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.test</id>
|
|
<name>Name</name>
|
|
<summary>Summary</summary>
|
|
<description>
|
|
<p>Description</p>
|
|
</description>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
</components>""")
|
|
b.wait_not_present(".pf-v5-c-empty-state")
|
|
b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('Summary')")
|
|
|
|
# First lay down some good XML so that we can later detect the reaction to broken XML.
|
|
reset()
|
|
|
|
# Unparsable
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
This <is <not XML.
|
|
""")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
reset()
|
|
|
|
# Not really AppStream
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<foo></foo>
|
|
""")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
reset()
|
|
|
|
# No origin
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components>
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.test</id>
|
|
<name>Name</name>
|
|
<summary>Summary</summary>
|
|
<description>
|
|
<p>Description</p>
|
|
</description>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
</components>""")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
reset()
|
|
|
|
# No package
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components origin="test">
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.test</id>
|
|
<name>Name</name>
|
|
<summary>Summary</summary>
|
|
<description>
|
|
<p>Description</p>
|
|
</description>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
</component>
|
|
</components>""")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
reset()
|
|
|
|
# No id
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components origin="test">
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<name>Name</name>
|
|
<summary>No description</summary>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
</components>""")
|
|
b.wait_visible(".pf-v5-c-empty-state")
|
|
reset()
|
|
|
|
# Error (launchable without type) in earlier entry, shouldn't affect the later entry
|
|
m.write("/usr/share/app-info/xmls/test.xml", """
|
|
<components origin="test">
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.test2</id>
|
|
<name>Name</name>
|
|
<summary>Summary</summary>
|
|
<description>
|
|
<p>Description 2</p>
|
|
</description>
|
|
<launchable>foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
<component type="addon">
|
|
<extends>org.cockpit_project.cockpit</extends>
|
|
<id>org.cockpit-project.test</id>
|
|
<name>Name</name>
|
|
<summary>Summary 2</summary>
|
|
<description>
|
|
<p>Description</p>
|
|
</description>
|
|
<launchable type="cockpit-manifest">foo</launchable>
|
|
<pkgname>foo</pkgname>
|
|
</component>
|
|
</components>""")
|
|
b.wait_not_present(".pf-v5-c-empty-state")
|
|
b.wait_visible(".app-list .pf-v5-c-data-list__item-row:contains('Summary 2')")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_main()
|