yunohost/src/tests/test_app_catalog.py
2023-02-01 17:10:08 +00:00

296 lines
9 KiB
Python

import os
import pytest
import requests
import requests_mock
import glob
import shutil
from moulinette import m18n
from moulinette.utils.filesystem import read_json, write_to_json, write_to_yaml
from yunohost.utils.error import YunohostError
from yunohost.app_catalog import (
_initialize_apps_catalog_system,
_read_apps_catalog_list,
_update_apps_catalog,
_actual_apps_catalog_api_url,
_load_apps_catalog,
app_catalog,
logger,
APPS_CATALOG_CACHE,
APPS_CATALOG_CONF,
APPS_CATALOG_API_VERSION,
APPS_CATALOG_DEFAULT_URL,
)
APPS_CATALOG_DEFAULT_URL_FULL = _actual_apps_catalog_api_url(APPS_CATALOG_DEFAULT_URL)
DUMMY_APP_CATALOG = """{
"apps": {
"foo": {"id": "foo", "level": 4, "category": "yolo", "manifest":{"description": "Foo"}},
"bar": {"id": "bar", "level": 7, "category": "swag", "manifest":{"description": "Bar"}}
},
"categories": [
{"id": "yolo", "description": "YoLo", "title": {"en": "Yolo"}},
{"id": "swag", "description": "sWaG", "title": {"en": "Swag"}}
]
}
"""
class AnyStringWith(str):
def __eq__(self, other):
return self in other
def setup_function(function):
# Clear apps catalog cache
shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
# Clear apps_catalog conf
if os.path.exists(APPS_CATALOG_CONF):
os.remove(APPS_CATALOG_CONF)
def teardown_function(function):
# Clear apps catalog cache
# Otherwise when using apps stuff after running the test,
# we'll still have the dummy unusable list
shutil.rmtree(APPS_CATALOG_CACHE, ignore_errors=True)
#
# ################################################
#
def test_apps_catalog_init(mocker):
# Cache is empty
assert not glob.glob(APPS_CATALOG_CACHE + "/*")
# Conf doesn't exist yet
assert not os.path.exists(APPS_CATALOG_CONF)
# Initialize ...
mocker.spy(m18n, "n")
_initialize_apps_catalog_system()
m18n.n.assert_any_call("apps_catalog_init_success")
# And a conf with at least one list
assert os.path.exists(APPS_CATALOG_CONF)
apps_catalog_list = _read_apps_catalog_list()
assert len(apps_catalog_list)
# Cache is expected to still be empty though
# (if we did update the apps_catalog during init,
# we couldn't differentiate easily exceptions
# related to lack of network connectivity)
assert not glob.glob(APPS_CATALOG_CACHE + "/*")
def test_apps_catalog_emptylist():
# Initialize ...
_initialize_apps_catalog_system()
# Let's imagine somebody removed the default apps catalog because uh idk they dont want to use our default apps catalog
os.system("rm %s" % APPS_CATALOG_CONF)
os.system("touch %s" % APPS_CATALOG_CONF)
apps_catalog_list = _read_apps_catalog_list()
assert not len(apps_catalog_list)
def test_apps_catalog_update_nominal(mocker):
# Initialize ...
_initialize_apps_catalog_system()
# Cache is empty
assert not glob.glob(APPS_CATALOG_CACHE + "/*")
# Update
with requests_mock.Mocker() as m:
_actual_apps_catalog_api_url,
# Mock the server response with a dummy apps catalog
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
mocker.spy(m18n, "n")
_update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_updating")
m18n.n.assert_any_call("apps_catalog_update_success")
# Cache shouldn't be empty anymore empty
assert glob.glob(APPS_CATALOG_CACHE + "/*")
# And if we load the catalog, we sould find
# - foo and bar as apps (unordered),
# - yolo and swag as categories (ordered)
catalog = app_catalog(with_categories=True)
assert "apps" in catalog
assert set(catalog["apps"].keys()) == {"foo", "bar"}
assert "categories" in catalog
assert [c["id"] for c in catalog["categories"]] == ["yolo", "swag"]
def test_apps_catalog_update_404(mocker):
# Initialize ...
_initialize_apps_catalog_system()
with requests_mock.Mocker() as m:
# 404 error
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, status_code=404)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
_update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_failed_to_download")
def test_apps_catalog_update_timeout(mocker):
# Initialize ...
_initialize_apps_catalog_system()
with requests_mock.Mocker() as m:
# Timeout
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.ConnectTimeout
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
_update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_failed_to_download")
def test_apps_catalog_update_sslerror(mocker):
# Initialize ...
_initialize_apps_catalog_system()
with requests_mock.Mocker() as m:
# SSL error
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, exc=requests.exceptions.SSLError
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
_update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_failed_to_download")
def test_apps_catalog_update_corrupted(mocker):
# Initialize ...
_initialize_apps_catalog_system()
with requests_mock.Mocker() as m:
# Corrupted json
m.register_uri(
"GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG[:-2]
)
with pytest.raises(YunohostError):
mocker.spy(m18n, "n")
_update_apps_catalog()
m18n.n.assert_any_call("apps_catalog_failed_to_download")
def test_apps_catalog_load_with_empty_cache(mocker):
# Initialize ...
_initialize_apps_catalog_system()
# Cache is empty
assert not glob.glob(APPS_CATALOG_CACHE + "/*")
# Update
with requests_mock.Mocker() as m:
# Mock the server response with a dummy apps catalog
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
# Try to load the apps catalog
# This should implicitly trigger an update in the background
mocker.spy(m18n, "n")
app_dict = _load_apps_catalog()["apps"]
m18n.n.assert_any_call("apps_catalog_obsolete_cache")
m18n.n.assert_any_call("apps_catalog_update_success")
# Cache shouldn't be empty anymore empty
assert glob.glob(APPS_CATALOG_CACHE + "/*")
assert "foo" in app_dict.keys()
assert "bar" in app_dict.keys()
def test_apps_catalog_load_with_conflicts_between_lists(mocker):
# Initialize ...
_initialize_apps_catalog_system()
conf = [
{"id": "default", "url": APPS_CATALOG_DEFAULT_URL},
{
"id": "default2",
"url": APPS_CATALOG_DEFAULT_URL.replace("yunohost.org", "yolohost.org"),
},
]
write_to_yaml(APPS_CATALOG_CONF, conf)
# Update
with requests_mock.Mocker() as m:
# Mock the server response with a dummy apps catalog
# + the same apps catalog for the second list
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
m.register_uri(
"GET",
APPS_CATALOG_DEFAULT_URL_FULL.replace("yunohost.org", "yolohost.org"),
text=DUMMY_APP_CATALOG,
)
# Try to load the apps catalog
# This should implicitly trigger an update in the background
mocker.spy(logger, "warning")
app_dict = _load_apps_catalog()["apps"]
logger.warning.assert_any_call(AnyStringWith("Duplicate"))
# Cache shouldn't be empty anymore empty
assert glob.glob(APPS_CATALOG_CACHE + "/*")
assert "foo" in app_dict.keys()
assert "bar" in app_dict.keys()
def test_apps_catalog_load_with_oudated_api_version(mocker):
# Initialize ...
_initialize_apps_catalog_system()
# Update
with requests_mock.Mocker() as m:
mocker.spy(m18n, "n")
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
_update_apps_catalog()
# Cache shouldn't be empty anymore empty
assert glob.glob(APPS_CATALOG_CACHE + "/*")
# Tweak the cache to replace the from_api_version with a different one
for cache_file in glob.glob(APPS_CATALOG_CACHE + "/*"):
cache_json = read_json(cache_file)
assert cache_json["from_api_version"] == APPS_CATALOG_API_VERSION
cache_json["from_api_version"] = 0
write_to_json(cache_file, cache_json)
# Update
with requests_mock.Mocker() as m:
# Mock the server response with a dummy apps catalog
m.register_uri("GET", APPS_CATALOG_DEFAULT_URL_FULL, text=DUMMY_APP_CATALOG)
mocker.spy(m18n, "n")
app_dict = _load_apps_catalog()["apps"]
m18n.n.assert_any_call("apps_catalog_update_success")
assert "foo" in app_dict.keys()
assert "bar" in app_dict.keys()
# Check that we indeed have the new api number in cache
for cache_file in glob.glob(APPS_CATALOG_CACHE + "/*"):
cache_json = read_json(cache_file)
assert cache_json["from_api_version"] == APPS_CATALOG_API_VERSION