Skip to content

Commit

Permalink
[Profiles] validate imported profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
Guillaume De Saint Martin authored and GuillaumeDSM committed Mar 8, 2023
1 parent 202c2ba commit e05ba7e
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 9 deletions.
6 changes: 6 additions & 0 deletions octobot_commons/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class ProfileRemovalError(Exception):
"""


class ProfileImportError(Exception):
"""
Profile related Exception: raised when the imported profile is invalid
"""


class ConfigEvaluatorError(Exception):
"""
Evaluator config related Exception
Expand Down
27 changes: 22 additions & 5 deletions octobot_commons/profiles/profile_sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import uuid
import time
import requests
import jsonschema
import octobot_commons.constants as constants
import octobot_commons.logging as bot_logging
import octobot_commons.errors as errors
Expand Down Expand Up @@ -76,16 +77,18 @@ def install_profile(
is_imported: bool,
origin_url: str = None,
quite: bool = False,
profile_schema: str = None,
) -> Profile:
"""
Installs the given profile export archive into the user's profile directory
:param import_path: path to the profile zipped archive
:param profile_name: name of the profile folder
:param bot_install_path: path to the octobot installation
:param replace_if_exists: when True erase the profile with the same name if it exists
:param is_imported: when True erase the profile is set as imported
:param is_imported: when True the profile is set as imported
:param origin_url: url the profile is coming from (if relevant)
:param quite: when True, only log errors
:param profile_schema: the schema to validate profile against
:return: The created profile
"""
logger = bot_logging.get_logger("ProfileSharing")
Expand All @@ -98,24 +101,35 @@ def install_profile(
if not quite:
logger.info(f"{action}ing {profile_name} profile.")
_import_profile_files(import_path, target_import_path)
profile = Profile(target_import_path).read_config()
profile = Profile(target_import_path, schema_path=profile_schema).read_config()
profile.imported = is_imported
profile.origin_url = origin_url
_ensure_unique_profile_id(profile)
if is_imported:
try:
profile.validate()
except jsonschema.exceptions.ValidationError as err:
shutil.rmtree(target_import_path)
raise errors.ProfileImportError(
f"Invalid imported profile: {err.message} in '{'/'.join(err.absolute_path)}'"
) from err
profile.save()
if not quite:
logger.info(f"{action}ed {profile.name} ({profile_name}) profile.")
return profile


def import_profile(
import_path: str,
profile_schema: str,
name: str = None,
bot_install_path: str = ".",
origin_url: str = None,
) -> Profile:
"""
Imports the given profile export archive into the user's profile directory with the "imported_" prefix
:param import_path: path to the profile zipped archive
:param profile_schema: the schema to validate profile against
:param name: name of the profile folder
:param bot_install_path: path to the octobot installation
:param origin_url: url the profile is coming from
Expand All @@ -129,6 +143,7 @@ def import_profile(
False,
True,
origin_url=origin_url,
profile_schema=profile_schema,
)
if profile.name != temp_profile_name:
profile.rename_folder(_get_unique_profile_folder_from_name(profile), False)
Expand All @@ -152,17 +167,20 @@ def download_profile(url, target_file, timeout=60):
return target_file


def download_and_install_profile(download_url):
def download_and_install_profile(download_url, profile_schema):
"""
:param download_url: profile url
:param profile_schema: the schema to validate profile against
:return: the installed profile, None if an error occurred
"""
logger = bot_logging.get_logger("ProfileSharing")
name = download_url.split("/")[-1]
file_path = None
try:
file_path = download_profile(download_url, name)
profile = import_profile(file_path, name=name, origin_url=download_url)
profile = import_profile(
file_path, profile_schema, name=name, origin_url=download_url
)
logger.info(
f"Downloaded and installed {profile.name} from {profile.origin_url}"
)
Expand Down Expand Up @@ -311,4 +329,3 @@ def _ensure_unique_profile_id(profile) -> None:
while profile.profile_id in ids and iteration < 100:
profile.profile_id = str(uuid.uuid4())
iteration += 1
profile.save()
7 changes: 7 additions & 0 deletions tests/profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import os

import pytest
import pathlib
import octobot_commons.profiles as profiles
Expand All @@ -30,3 +32,8 @@ def get_profiles_path():
@pytest.fixture
def profile():
return profiles.Profile(get_profile_path())


@pytest.fixture
def invalid_profile():
return profiles.Profile(os.path.join(get_profile_path(), "invalid_profile"))
19 changes: 15 additions & 4 deletions tests/profiles/test_profile_sharing.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,18 @@
import contextlib
import mock
import pathlib

import pytest

import octobot_commons.constants as constants
import octobot_commons.errors as commons_errors
import octobot_commons.profiles as profiles
import octobot_commons.profiles.profile_sharing as profile_sharing
from octobot_commons.profiles.profile_sharing import _get_unique_profile_folder, _ensure_unique_profile_id, \
_get_profile_name
import octobot_commons.tests.test_config as test_config

from tests.profiles import profile, get_profile_path
from tests.profiles import profile, get_profile_path, invalid_profile


def test_export_profile(profile):
Expand Down Expand Up @@ -93,12 +97,13 @@ def test_export_profile_with_existing_file(profile):
)


def test_import_install_profile(profile):
def test_import_install_profile(profile, invalid_profile):
export_path = os.path.join(test_config.TEST_FOLDER, "super_profile")
exported_file = f"{export_path}.zip"
spec_tentacles_config = os.path.join(get_profile_path(), "specific_config")
tentacles_config = os.path.join(get_profile_path(), "tentacles_config.json")
other_profile = os.path.join(constants.USER_PROFILES_FOLDER, "default")
profile_schema = os.path.join(test_config.TEST_CONFIG_FOLDER, "profile_schema.json")
with _cleaned_tentacles(export_path,
exported_file,
tentacles_config,
Expand All @@ -114,7 +119,7 @@ def test_import_install_profile(profile):
imported_profile_path = os.path.join(constants.USER_PROFILES_FOLDER, "default")
with mock.patch.object(profile_sharing, "_ensure_unique_profile_id", mock.Mock()) \
as _ensure_unique_profile_id_mock:
imported_profile = profiles.import_profile(exported_file, origin_url="plop.wow")
imported_profile = profiles.import_profile(exported_file, profile_schema, origin_url="plop.wow")
assert isinstance(imported_profile, profiles.Profile)
profile.read_config()
assert profile.name == imported_profile.name
Expand All @@ -131,11 +136,16 @@ def test_import_install_profile(profile):
os.path.isfile(os.path.join(dir_path, f))
for f in files
)
assert isinstance(profiles.import_profile(exported_file), profiles.Profile)
assert isinstance(profiles.import_profile(exported_file, profile_schema), profiles.Profile)
assert os.path.isdir(f"{imported_profile_path}_2")
assert os.path.isdir(imported_profile_path)
assert not os.path.isdir(f"{imported_profile_path}_3")

# now with invalid profile
profiles.export_profile(invalid_profile, export_path)
with pytest.raises(commons_errors.ProfileImportError):
profiles.import_profile(exported_file, profile_schema)


def test_get_unique_profile_folder(profile):
assert _get_unique_profile_folder(profile.config_file()) == f"{profile.config_file()}_2"
Expand All @@ -159,6 +169,7 @@ def test_ensure_unique_profile_id(profile):
shutil.copytree(profile.path, other_profile_path)
other_profile = profiles.Profile(other_profile_path).read_config()
_ensure_unique_profile_id(other_profile)
other_profile.save()
ids = profiles.Profile.get_all_profiles_ids(profiles_path)
assert len(ids) == 2
# changed new profile id
Expand Down
50 changes: 50 additions & 0 deletions tests/static/invalid_profile/profile.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"profile": {
"avatar": "default_profile.png",
"description": "OctoBot default profile.",
"id": "invalid_profile",
"name": "default",
"origin_url": "https://default.url"
},
"config": {
"crypto-currencies": {
"Bitcoin": {
"pairs": [
"BTC/USDT"
]
},
"plop": {
"pairs": [
"BTC/USDT"
],
"config": {
"i should not be there": true
}
}
},
"exchanges": {
"binance": {
"enabled": true
}
},
"trading": {
"reference-market": "BTC",
"risk": 0.5
},
"trader": {
"enabled": false,
"load-trade-history": true
},
"trader-simulator": {
"enabled": true,
"fees": {
"maker": 0.1,
"taker": 0.1
},
"starting-portfolio": {
"BTC": 10,
"USDT": 1000
}
}
}
}
Loading

0 comments on commit e05ba7e

Please sign in to comment.