From 488275422148a9fc79338c1a16bf22117178da4b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 7 Aug 2019 02:28:37 +0200 Subject: [PATCH 1/6] [mod] stop apps upgrade if one upgrade fail --- src/yunohost/app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 0784f1ecc..d64baeaa3 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -667,7 +667,6 @@ def app_upgrade(app=[], url=None, file=None): # Execute App upgrade script os.system('chown -hR admin: %s' % INSTALL_TMP) - try: upgrade_retcode = hook_exec(extracted_app_folder + '/scripts/upgrade', args=args_list, env=env_dict)[0] From 3eb089ffc062b3f3cccd833103b3b218bb35e3ea Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 18 Aug 2019 01:41:34 +0200 Subject: [PATCH 2/6] Add unit/functional tests for apps --- src/yunohost/tests/test_apps.py | 248 ++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/yunohost/tests/test_apps.py diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py new file mode 100644 index 000000000..3407c4f39 --- /dev/null +++ b/src/yunohost/tests/test_apps.py @@ -0,0 +1,248 @@ +import glob +import os +import pytest +import shutil +import requests + +from moulinette import m18n +from moulinette.utils.filesystem import mkdir + +from yunohost.app import app_install, app_remove, app_ssowatconf, _is_installed +from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list +from yunohost.utils.error import YunohostError +from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps + + +MAIN_DOMAIN = _get_maindomain() + + +def setup_function(function): + + clean() + +def teardown_function(function): + + clean() + +def clean(): + + # Make sure we have a ssowat + os.system("mkdir -p /etc/ssowat/") + app_ssowatconf() + + if _is_installed("legacy_app"): + app_remove("legacy_app") + + to_remove = [] + to_remove += glob.glob("/etc/nginx/conf.d/*.d/*legacy*") + for filepath in to_remove: + os.remove(filepath) + + to_remove = [] + to_remove += glob.glob("/etc/yunohost/apps/*legacy_app*") + to_remove += glob.glob("/var/www/*legacy*") + for folderpath in to_remove: + shutil.rmtree(folderpath, ignore_errors=True) + + +@pytest.fixture(autouse=True) +def check_LDAP_db_integrity_call(): + check_LDAP_db_integrity() + yield + check_LDAP_db_integrity() + + +@pytest.fixture(autouse=True) +def check_permission_for_apps_call(): + check_permission_for_apps() + yield + check_permission_for_apps() + +@pytest.fixture(scope="session") +def secondary_domain(request): + + if "example.test" not in domain_list()["domains"]: + domain_add("example.test") + + def remove_example_domain(): + domain_remove("example.test") + request.addfinalizer(remove_example_domain) + + return "example.test" + + +# +# Helpers # +# + +def app_expected_files(domain, app): + + yield "/etc/nginx/conf.d/%s.d/%s.conf" % (domain, app) + yield "/var/www/%s/index.html" % app + yield "/etc/yunohost/apps/%s/settings.yml" % app + yield "/etc/yunohost/apps/%s/manifest.json" % app + yield "/etc/yunohost/apps/%s/scripts/install" % app + yield "/etc/yunohost/apps/%s/scripts/remove" % app + yield "/etc/yunohost/apps/%s/scripts/backup" % app + yield "/etc/yunohost/apps/%s/scripts/restore" % app + + +def app_is_installed(domain, app): + + return _is_installed(app) and all(os.path.exists(f) for f in app_expected_files(domain, app)) + + +def app_is_not_installed(domain, app): + + return not _is_installed(app) and not all(os.path.exists(f) for f in app_expected_files(domain, app)) + + +def app_is_exposed_on_http(domain, path, message_in_page): + + try: + r = requests.get("http://127.0.0.1" + path + "/", headers={"Host": domain}, timeout=10) + return r.status_code == 200 and message_in_page in r.text + except Exception: + return False + + +def install_legacy_app(domain, path): + + app_install("./tests/apps/legacy_app_ynh", + args="domain=%s&path=%s" % (domain, path), + force=True) + + +def test_legacy_app_install_main_domain(): + + install_legacy_app(MAIN_DOMAIN, "/legacy") + + assert app_is_installed(MAIN_DOMAIN, "legacy_app") + assert app_is_exposed_on_http(MAIN_DOMAIN, "/legacy", "This is a dummy app") + + app_remove("legacy_app") + + assert app_is_not_installed(MAIN_DOMAIN, "legacy_app") + + +def test_legacy_app_install_secondary_domain(secondary_domain): + + install_legacy_app(secondary_domain, "/legacy") + + assert app_is_installed(secondary_domain, "legacy_app") + assert app_is_exposed_on_http(secondary_domain, "/legacy", "This is a dummy app") + + app_remove("legacy_app") + + assert app_is_not_installed(secondary_domain, "legacy_app") + + +def test_legacy_app_install_secondary_domain_on_root(secondary_domain): + + install_legacy_app(secondary_domain, "/") + + assert app_is_installed(secondary_domain, "legacy_app") + assert app_is_exposed_on_http(secondary_domain, "/", "This is a dummy app") + + app_remove("legacy_app") + + assert app_is_not_installed(secondary_domain, "legacy_app") + + +def test_legacy_app_install_private(secondary_domain): + + install_legacy_app(secondary_domain, "/legacy") + + settings = open("/etc/yunohost/apps/legacy_app/settings.yml", "r").read() + new_settings = settings.replace("\nunprotected_uris: /", "") + assert new_settings != settings + open("/etc/yunohost/apps/legacy_app/settings.yml", "w").write(new_settings) + app_ssowatconf() + + assert app_is_installed(secondary_domain, "legacy_app") + assert not app_is_exposed_on_http(secondary_domain, "/legacy", "This is a dummy app") + + app_remove("legacy_app") + + assert app_is_not_installed(secondary_domain, "legacy_app") + + +def test_legacy_app_install_unknown_domain(): + + with pytest.raises(YunohostError): + install_legacy_app("whatever.nope", "/legacy") + # TODO check error message + + assert app_is_not_installed("whatever.nope", "legacy_app") + + +def test_legacy_app_install_multiple_instances(secondary_domain): + + install_legacy_app(secondary_domain, "/foo") + install_legacy_app(secondary_domain, "/bar") + + assert app_is_installed(secondary_domain, "legacy_app") + assert app_is_exposed_on_http(secondary_domain, "/foo", "This is a dummy app") + + assert app_is_installed(secondary_domain, "legacy_app__2") + assert app_is_exposed_on_http(secondary_domain, "/bar", "This is a dummy app") + + app_remove("legacy_app") + + assert app_is_not_installed(secondary_domain, "legacy_app") + assert app_is_installed(secondary_domain, "legacy_app__2") + + app_remove("legacy_app__2") + + assert app_is_not_installed(secondary_domain, "legacy_app") + assert app_is_not_installed(secondary_domain, "legacy_app__2") + + +def test_legacy_app_install_path_unavailable(secondary_domain): + + # These will be removed in teardown + install_legacy_app(secondary_domain, "/legacy") + + with pytest.raises(YunohostError): + install_legacy_app(secondary_domain, "/") + # TODO check error message + + assert app_is_installed(secondary_domain, "legacy_app") + assert app_is_not_installed(secondary_domain, "legacy_app__2") + + +def test_legacy_app_failed_install(secondary_domain): + + mkdir("/var/www/legacy_app/", 0o750) + + with pytest.raises(YunohostError): + install_legacy_app(secondary_domain, "/legacy") + # TODO check error message + + assert app_is_not_installed(secondary_domain, "legacy_app") + + +def test_legacy_app_install_with_nginx_down(secondary_domain): + + os.system("systemctl stop nginx") + + with pytest.raises(YunohostError): + install_legacy_app(secondary_domain, "/legacy") + + os.system("systemctl start nginx") + + +def test_legacy_app_failed_remove(): + + # FIXME What's supposed to happen lol + raise NotImplementedError + + +def test_legacy_app_install_fucksup_nginx(): + + # FIXME What's supposed to happen lol + raise NotImplementedError + +def test_legacy_app_install_with_dpkg_fuckedup(): + + raise NotImplementedError From 799c68f1a88faa1036c874007c09c6e3e8d40620 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sat, 24 Aug 2019 00:15:42 +0200 Subject: [PATCH 3/6] Moar tests for apps breaking the system --- src/yunohost/tests/test_apps.py | 119 +++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 25 deletions(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index 3407c4f39..1dcffbf0e 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -13,9 +13,6 @@ from yunohost.utils.error import YunohostError from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps -MAIN_DOMAIN = _get_maindomain() - - def setup_function(function): clean() @@ -33,17 +30,24 @@ def clean(): if _is_installed("legacy_app"): app_remove("legacy_app") + if _is_installed("break_yo_system"): + app_remove("break_yo_system") + to_remove = [] to_remove += glob.glob("/etc/nginx/conf.d/*.d/*legacy*") + to_remove += glob.glob("/etc/nginx/conf.d/*.d/*break_yo_system*") for filepath in to_remove: os.remove(filepath) to_remove = [] to_remove += glob.glob("/etc/yunohost/apps/*legacy_app*") + to_remove += glob.glob("/etc/yunohost/apps/*break_yo_system*") to_remove += glob.glob("/var/www/*legacy*") for folderpath in to_remove: shutil.rmtree(folderpath, ignore_errors=True) + os.system("systemctl start nginx") + @pytest.fixture(autouse=True) def check_LDAP_db_integrity_call(): @@ -78,13 +82,12 @@ def secondary_domain(request): def app_expected_files(domain, app): yield "/etc/nginx/conf.d/%s.d/%s.conf" % (domain, app) - yield "/var/www/%s/index.html" % app + if app.startswith("legacy_app"): + yield "/var/www/%s/index.html" % app yield "/etc/yunohost/apps/%s/settings.yml" % app yield "/etc/yunohost/apps/%s/manifest.json" % app yield "/etc/yunohost/apps/%s/scripts/install" % app yield "/etc/yunohost/apps/%s/scripts/remove" % app - yield "/etc/yunohost/apps/%s/scripts/backup" % app - yield "/etc/yunohost/apps/%s/scripts/restore" % app def app_is_installed(domain, app): @@ -113,16 +116,25 @@ def install_legacy_app(domain, path): force=True) +def install_break_yo_system(domain, breakwhat): + + app_install("./tests/apps/break_yo_system_ynh", + args="domain=%s&breakwhat=%s" % (domain, breakwhat), + force=True) + + def test_legacy_app_install_main_domain(): - install_legacy_app(MAIN_DOMAIN, "/legacy") + main_domain = _get_maindomain() - assert app_is_installed(MAIN_DOMAIN, "legacy_app") - assert app_is_exposed_on_http(MAIN_DOMAIN, "/legacy", "This is a dummy app") + install_legacy_app(main_domain, "/legacy") + + assert app_is_installed(main_domain, "legacy_app") + assert app_is_exposed_on_http(main_domain, "/legacy", "This is a dummy app") app_remove("legacy_app") - assert app_is_not_installed(MAIN_DOMAIN, "legacy_app") + assert app_is_not_installed(main_domain, "legacy_app") def test_legacy_app_install_secondary_domain(secondary_domain): @@ -211,15 +223,10 @@ def test_legacy_app_install_path_unavailable(secondary_domain): assert app_is_not_installed(secondary_domain, "legacy_app__2") -def test_legacy_app_failed_install(secondary_domain): - - mkdir("/var/www/legacy_app/", 0o750) +def test_legacy_app_install_bad_args(): with pytest.raises(YunohostError): - install_legacy_app(secondary_domain, "/legacy") - # TODO check error message - - assert app_is_not_installed(secondary_domain, "legacy_app") + install_legacy_app("this.domain.does.not.exists", "/legacy") def test_legacy_app_install_with_nginx_down(secondary_domain): @@ -229,20 +236,82 @@ def test_legacy_app_install_with_nginx_down(secondary_domain): with pytest.raises(YunohostError): install_legacy_app(secondary_domain, "/legacy") - os.system("systemctl start nginx") + +def test_legacy_app_failed_install(secondary_domain): + + # This will conflict with the folder that the app + # attempts to create, making the install fail + mkdir("/var/www/legacy_app/", 0o750) + + with pytest.raises(YunohostError): + install_legacy_app(secondary_domain, "/legacy") + # TODO check error message + + assert app_is_not_installed(secondary_domain, "legacy_app") -def test_legacy_app_failed_remove(): +def test_legacy_app_failed_remove(secondary_domain): - # FIXME What's supposed to happen lol - raise NotImplementedError + install_legacy_app(secondary_domain, "/legacy") + + # The remove script runs with set -eu and attempt to remove this + # file without -f, so will fail if it's not there ;) + os.remove("/etc/nginx/conf.d/%s.d/%s.conf" % (secondary_domain, "legacy_app")) + with pytest.raises(YunohostError): + app_remove("legacy") + + # + # Well here, we hit the classical issue where if an app removal script + # fails, so far there's no obvious way to make sure that all files related + # to this app got removed ... + # + assert app_is_not_installed(secondary_domain, "legacy") -def test_legacy_app_install_fucksup_nginx(): +def test_systemfuckedup_during_app_install(secondary_domain): - # FIXME What's supposed to happen lol - raise NotImplementedError + with pytest.raises(YunohostError): + install_break_yo_system(secondary_domain, breakwhat="install") + os.system("nginx -t") + os.system("systemctl status nginx") -def test_legacy_app_install_with_dpkg_fuckedup(): + assert app_is_not_installed(secondary_domain, "break_yo_system") + + +def test_systemfuckedup_during_app_remove(secondary_domain): + + install_break_yo_system(secondary_domain, breakwhat="remove") + + with pytest.raises(YunohostError): + app_remove("break_yo_system") + os.system("nginx -t") + os.system("systemctl status nginx") + + assert app_is_not_installed(secondary_domain, "break_yo_system") + + +def test_systemfuckedup_during_app_install_and_remove(secondary_domain): + + with pytest.raises(YunohostError): + install_break_yo_system(secondary_domain, breakwhat="everything") + + assert app_is_not_installed(secondary_domain, "break_yo_system") + + +def test_systemfuckedup_during_app_upgrade(secondary_domain): raise NotImplementedError + + install_break_yo_system(secondary_domain, breakwhat="upgrade") + + #app_upgrade("break_yo_system", ...) + + +def test_failed_multiple_app_upgrade(secondary_domain): + + raise NotImplementedError + + install_legacy_app(secondary_domain, "/legacy") + install_break_yo_system(secondary_domain, breakwhat="upgrade") + + app_upgrade(["break_yo_system", "legacy"]) From 28c73cb336aeef910f460308eb89465078ea2aab Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 02:11:37 +0200 Subject: [PATCH 4/6] Implement those remaining tests --- src/yunohost/tests/test_apps.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src/yunohost/tests/test_apps.py b/src/yunohost/tests/test_apps.py index 1dcffbf0e..9c85df1e9 100644 --- a/src/yunohost/tests/test_apps.py +++ b/src/yunohost/tests/test_apps.py @@ -7,7 +7,7 @@ import requests from moulinette import m18n from moulinette.utils.filesystem import mkdir -from yunohost.app import app_install, app_remove, app_ssowatconf, _is_installed +from yunohost.app import app_install, app_remove, app_ssowatconf, _is_installed, app_upgrade from yunohost.domain import _get_maindomain, domain_add, domain_remove, domain_list from yunohost.utils.error import YunohostError from yunohost.tests.test_permission import check_LDAP_db_integrity, check_permission_for_apps @@ -27,12 +27,15 @@ def clean(): os.system("mkdir -p /etc/ssowat/") app_ssowatconf() - if _is_installed("legacy_app"): - app_remove("legacy_app") - + # Gotta first remove break yo system + # because some remaining stuff might + # make the other app_remove crashs ;P if _is_installed("break_yo_system"): app_remove("break_yo_system") + if _is_installed("legacy_app"): + app_remove("legacy_app") + to_remove = [] to_remove += glob.glob("/etc/nginx/conf.d/*.d/*legacy*") to_remove += glob.glob("/etc/nginx/conf.d/*.d/*break_yo_system*") @@ -46,6 +49,7 @@ def clean(): for folderpath in to_remove: shutil.rmtree(folderpath, ignore_errors=True) + os.system("systemctl reset-failed nginx") # Reset failed quota for service to avoid running into start-limit rate ? os.system("systemctl start nginx") @@ -300,18 +304,18 @@ def test_systemfuckedup_during_app_install_and_remove(secondary_domain): def test_systemfuckedup_during_app_upgrade(secondary_domain): - raise NotImplementedError - install_break_yo_system(secondary_domain, breakwhat="upgrade") - #app_upgrade("break_yo_system", ...) + with pytest.raises(YunohostError): + app_upgrade("break_yo_system", file="./tests/apps/break_yo_system_ynh") def test_failed_multiple_app_upgrade(secondary_domain): - raise NotImplementedError - install_legacy_app(secondary_domain, "/legacy") install_break_yo_system(secondary_domain, breakwhat="upgrade") - app_upgrade(["break_yo_system", "legacy"]) + with pytest.raises(YunohostError): + app_upgrade(["break_yo_system", "legacy_app"], + file={"break_yo_system": "./tests/apps/break_yo_system_ynh", + "legacy": "./tests/apps/legacy_app_ynh"}) From a476deb7fbeb73b23d6282b869e07cbf23f316be Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 02:11:53 +0200 Subject: [PATCH 5/6] Tweak test conf for easier debugging --- src/yunohost/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/tests/conftest.py b/src/yunohost/tests/conftest.py index a2dc585bd..ce321933c 100644 --- a/src/yunohost/tests/conftest.py +++ b/src/yunohost/tests/conftest.py @@ -41,11 +41,11 @@ def pytest_cmdline_main(config): root_handlers = set(handlers) # Define loggers level - level = 'INFO' + level = 'DEBUG' if config.option.yunodebug: tty_level = 'DEBUG' else: - tty_level = 'SUCCESS' + tty_level = 'INFO' # Custom logging configuration logging = { From aa3687ba029abae89f9a01d6e45df3ab843e93ea Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 15 Sep 2019 02:13:02 +0200 Subject: [PATCH 6/6] Small trick needed to be able to test chained app upgrades --- src/yunohost/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index d64baeaa3..6b75d82d0 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -620,7 +620,10 @@ def app_upgrade(app=[], url=None, file=None): app_dict = app_info(app_instance_name, raw=True) - if file: + if file and isinstance(file, dict): + # We use this dirty hack to test chained upgrades in unit/functional tests + manifest, extracted_app_folder = _extract_app_from_file(file[app_instance_name]) + elif file: manifest, extracted_app_folder = _extract_app_from_file(file) elif url: manifest, extracted_app_folder = _fetch_app_from_git(url)