diff --git a/kdb-bot/cpl-workspace.json b/kdb-bot/cpl-workspace.json index f16de580..62ee582a 100644 --- a/kdb-bot/cpl-workspace.json +++ b/kdb-bot/cpl-workspace.json @@ -14,29 +14,27 @@ "permission": "src/modules/permission/permission.json", "stats": "src/modules/stats/stats.json", "technician": "src/modules/technician/technician.json", + "ui-tests": "test/ui_tests/ui-tests.json", + "ui-tests-shared": "src/../test/ui_tests_shared/ui-tests-shared.json", + "ui-tests-tests": "test/ui_tests_tests/ui-tests-tests.json", "get-version": "tools/get_version/get-version.json", "post-build": "tools/post_build/post-build.json", "set-version": "tools/set_version/set-version.json" }, "Scripts": { + "test": "cpl run ui-tests", "sv": "cpl set-version $ARGS", "set-version": "cpl run set-version $ARGS; echo '';", - "gv": "cpl get-version", "get-version": "export VERSION=$(cpl run get-version); echo $VERSION;", - "pre-build": "cpl set-version $ARGS", "post-build": "cpl run post-build", - "pre-prod": "cpl build", "prod": "export KDB_ENVIRONMENT=production; export KDB_NAME=KDB-Prod; cpl start;", - "pre-stage": "cpl build", "stage": "export KDB_ENVIRONMENT=staging; export KDB_NAME=KDB-Stage; cpl start;", - "pre-dev": "cpl build", "dev": "export KDB_ENVIRONMENT=development; export KDB_NAME=KDB-Dev; cpl start;", - "docker-build": "cpl build $ARGS; docker build -t kdb-bot/kdb-bot:$(cpl gv) .;", "dc-up": "docker-compose up -d", "dc-down": "docker-compose down", diff --git a/kdb-bot/src/bot/startup_test_extension.py b/kdb-bot/src/bot/startup_test_extension.py new file mode 100644 index 00000000..6497e6b5 --- /dev/null +++ b/kdb-bot/src/bot/startup_test_extension.py @@ -0,0 +1,28 @@ +import os +from datetime import datetime +from typing import Callable, Type, Optional + +from cpl_core.application import StartupExtensionABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironmentABC + +from bot_core.configuration.bot_logging_settings import BotLoggingSettings +from bot_core.configuration.bot_settings import BotSettings +from modules.base.configuration.base_settings import BaseSettings +from modules.boot_log.configuration.boot_log_settings import BootLogSettings +from modules.level.configuration.level_settings import LevelSettings +from modules.permission.configuration.permission_settings import PermissionSettings + + +class StartupTestExtension(StartupExtensionABC): + + def __init__(self): + StartupExtensionABC.__init__(self) + + def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironmentABC): + # this shit has to be done here because we need settings in subsequent startup extensions + environment.set_working_directory(os.path.dirname(os.path.realpath(__file__))) + + def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): + pass diff --git a/kdb-bot/test/__init__.py b/kdb-bot/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/test/ui_tests/__init__.py b/kdb-bot/test/ui_tests/__init__.py new file mode 100644 index 00000000..52f86f25 --- /dev/null +++ b/kdb-bot/test/ui_tests/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/kdb-bot/test/ui_tests/config/appsettings.edrafts-pc.json b/kdb-bot/test/ui_tests/config/appsettings.edrafts-pc.json new file mode 100644 index 00000000..6db810e8 --- /dev/null +++ b/kdb-bot/test/ui_tests/config/appsettings.edrafts-pc.json @@ -0,0 +1,16 @@ +{ + "DatabaseSettings": { + "Host": "localhost", + "User": "kd_kdb", + "Password": "", + "Database": "keksdose_bot_dev", + "Charset": "utf8mb4", + "UseUnicode": "true", + "Buffered": "true", + "AuthPlugin": "mysql_native_password" + }, + "DiscordBot": { + "Token": "", + "Prefix": "!kab-e " + } +} \ No newline at end of file diff --git a/kdb-bot/test/ui_tests/config/appsettings.json b/kdb-bot/test/ui_tests/config/appsettings.json new file mode 100644 index 00000000..8eba0395 --- /dev/null +++ b/kdb-bot/test/ui_tests/config/appsettings.json @@ -0,0 +1,20 @@ +{ + "TimeFormatSettings": { + "DateFormat": "%Y-%m-%d", + "TimeFormat": "%H:%M:%S", + "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", + "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" + }, + "LoggingSettings": { + "Path": "logs/$date_now/", + "Filename": "bot.log", + "ConsoleLogLevel": "ERROR", + "FileLogLevel": "WARN" + }, + "Translation": { + "DefaultLanguage": "de", + "Languages": [ + "de" + ] + } +} \ No newline at end of file diff --git a/kdb-bot/test/ui_tests/main.py b/kdb-bot/test/ui_tests/main.py new file mode 100644 index 00000000..6ad30424 --- /dev/null +++ b/kdb-bot/test/ui_tests/main.py @@ -0,0 +1,34 @@ +import asyncio + +from cpl_core.application import ApplicationABC +from cpl_core.application import ApplicationBuilder + +from bot.startup_test_extension import StartupTestExtension +from ui_tests.startup import Startup +from ui_tests_shared.declarations import Declarations + +try: + from test_application import TestApplication +except ImportError: + from .test_application import TestApplication + + +def get_app() -> ApplicationABC: + app_builder = ApplicationBuilder(TestApplication) \ + .use_extension(StartupTestExtension) \ + .use_startup(Startup) + app = app_builder.build() + Declarations.app = app + return app + + +async def main(): + app = get_app() + await app.run_async() + + +if __name__ == '__main__': + import nest_asyncio + + nest_asyncio.apply() + asyncio.run(main()) diff --git a/kdb-bot/test/ui_tests/startup.py b/kdb-bot/test/ui_tests/startup.py new file mode 100644 index 00000000..0d8a4907 --- /dev/null +++ b/kdb-bot/test/ui_tests/startup.py @@ -0,0 +1,56 @@ +import os +from typing import Optional + +from cpl_core.application import StartupABC +from cpl_core.configuration import ConfigurationABC +from cpl_core.database import DatabaseSettings +from cpl_core.database.context import DatabaseContext +from cpl_core.dependency_injection import ServiceProviderABC, ServiceCollectionABC +from cpl_core.environment import ApplicationEnvironment +from cpl_discord import get_discord_collection +from cpl_discord.discord_event_types_enum import DiscordEventTypesEnum + +from ui_tests.test_on_ready_event import TestOnReadyEvent + + +class Startup(StartupABC): + + def __init__(self): + StartupABC.__init__(self) + + self._config: Optional[ConfigurationABC] = None + + def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironment) -> ConfigurationABC: + configuration.add_environment_variables('KDB-TEST_') + configuration.add_environment_variables('DISCORD_') + + cwd = os.path.dirname(os.path.realpath(__file__)) + configuration.add_json_file(f'{cwd}/config/appsettings.json', optional=False) + configuration.add_json_file(f'{cwd}/config/appsettings.{environment.host_name}.json', optional=True) + + self._config = configuration + return configuration + + def configure_services(self, services: ServiceCollectionABC, environment: ApplicationEnvironment) -> ServiceProviderABC: + services.add_logging() + services.add_translation() + db_settings: DatabaseSettings = self._config.get_configuration(DatabaseSettings) + db_settings_with_pw = DatabaseSettings() + pw = self._config.get_configuration('DB_PASSWORD') + db_settings_with_pw.from_dict({ + "Host": db_settings.host, + "User": db_settings.user, + "Password": '' if pw is None else pw, + "Database": db_settings.database, + "Charset": db_settings.charset, + "UseUnicode": db_settings.use_unicode, + "Buffered": db_settings.buffered, + "AuthPlugin": db_settings.auth_plugin + }) + services.add_db_context(DatabaseContext, db_settings_with_pw) + + services.add_discord() + dc = get_discord_collection(services) + dc.add_event(DiscordEventTypesEnum.on_ready.value, TestOnReadyEvent) + + return services.build_service_provider() diff --git a/kdb-bot/test/ui_tests/test_application.py b/kdb-bot/test/ui_tests/test_application.py new file mode 100644 index 00000000..a0201a3a --- /dev/null +++ b/kdb-bot/test/ui_tests/test_application.py @@ -0,0 +1,34 @@ +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_discord.application import DiscordBotApplicationABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslationSettings, TranslationServiceABC + + +class TestApplication(DiscordBotApplicationABC): + + def __init__(self, config: ConfigurationABC, services: ServiceProviderABC): + DiscordBotApplicationABC.__init__(self, config, services) + + self._config = config + self._services = services + + self._bot: DiscordBotServiceABC = services.get_service(DiscordBotServiceABC) + self._translation: TranslationServiceABC = services.get_service(TranslationServiceABC) + + @property + def config(self) -> ConfigurationABC: + return self._config + + @property + def services(self) -> ServiceProviderABC: + return self._services + + async def configure(self): + self._translation.load_by_settings(self._configuration.get_configuration(TranslationSettings)) + + async def main(self): + await self._bot.start_async() + + async def stop_async(self): + await self._bot.close() diff --git a/kdb-bot/test/ui_tests/test_on_ready_event.py b/kdb-bot/test/ui_tests/test_on_ready_event.py new file mode 100644 index 00000000..d2c5080d --- /dev/null +++ b/kdb-bot/test/ui_tests/test_on_ready_event.py @@ -0,0 +1,43 @@ +import os +import unittest + +from cpl_core.console import Console, ForegroundColorEnum +from cpl_core.dependency_injection import ServiceProviderABC +from cpl_core.logging import LoggerABC +from cpl_discord.events import OnReadyABC +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe + +from bot_core.abc.client_utils_service_abc import ClientUtilsServiceABC + + +class TestOnReadyEvent(OnReadyABC): + + def __init__( + self, + logger: LoggerABC, + bot: DiscordBotServiceABC, + services: ServiceProviderABC, + client_utils: ClientUtilsServiceABC, + t: TranslatePipe + ): + OnReadyABC.__init__(self) + + self._logger = logger + self._bot = bot + self._services = services + self._client_utils = client_utils + self._t = t + + async def on_ready(self): + Console.write_line('\nStarting tests:\n') + loader = unittest.TestLoader() + path = f'{os.path.dirname(os.path.realpath(__file__))}/../' + tests = loader.discover(path, pattern='*_test_case.py') + runner = unittest.TextTestRunner() + runner.run(tests) + # for cls in CommandTestABC.__subclasses__(): + # service: CommandTestABC = self._services.get_service(cls) + # await service.run(self._tests) + + await self._bot.close() diff --git a/kdb-bot/test/ui_tests/ui-tests.json b/kdb-bot/test/ui_tests/ui-tests.json new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/test/ui_tests_shared/__init__.py b/kdb-bot/test/ui_tests_shared/__init__.py new file mode 100644 index 00000000..ad5eca30 --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/kdb-bot/test/ui_tests_shared/chrome/linux/chromedriver b/kdb-bot/test/ui_tests_shared/chrome/linux/chromedriver new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/test/ui_tests_shared/command_test_case_with_app.py b/kdb-bot/test/ui_tests_shared/command_test_case_with_app.py new file mode 100644 index 00000000..38c05a83 --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/command_test_case_with_app.py @@ -0,0 +1,46 @@ +import time + +from cpl_discord.service import DiscordBotServiceABC +from cpl_translation import TranslatePipe +from selenium import webdriver +from selenium.common import NoSuchElementException, StaleElementReferenceException +from selenium.webdriver import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.remote.webelement import WebElement +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from ui_tests_shared.test_case_with_app import TestCaseWithApp + + +class CommandTestCaseWithApp(TestCaseWithApp): + _cmd: WebElement + + _bot = None + _t = None + + @classmethod + def setUpClass(cls): + TestCaseWithApp.setUpClass() + cls._bot = cls._services.get_service(DiscordBotServiceABC) + cls._t = cls._services.get_service(TranslatePipe) + + def send_command(self, cmd: str): + self._driver.get('https://discord.com/channels/910199451145076828/911578636899987526') + time.sleep(2) + + cmd_element_ident = (By.XPATH, '/html/body/div[1]/div[2]/div/div[1]/div/div[2]/div/div[1]/div/div/div[3]/div[2]/main/form/div/div[1]/div/div[3]/div/div[2]/div') + cmd_element = self._driver.find_element(*cmd_element_ident) + cmd_element.send_keys(f'/{cmd}') + time.sleep(2) + + ignored_exceptions = (NoSuchElementException, StaleElementReferenceException,) + WebDriverWait(self._driver, 20, ignored_exceptions=ignored_exceptions).until( + expected_conditions.presence_of_element_located(( + By.XPATH, + '/html/body/div[1]/div[2]/div/div[1]/div/div[2]/div/div[1]/div/div/div[3]/div[2]/main/form/div/div[2]/div/div/div[5]' + )) + ).click() + time.sleep(2) + self._driver.find_element(By.XPATH, '/html/body/div[1]/div[2]/div/div[1]/div/div[2]/div/div[1]/div/div/div[3]/div/main/form/div/div[2]/div/div[2]/div/div').send_keys( + Keys.ENTER) diff --git a/kdb-bot/test/ui_tests_shared/declarations.py b/kdb-bot/test/ui_tests_shared/declarations.py new file mode 100644 index 00000000..17c6a27f --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/declarations.py @@ -0,0 +1,7 @@ +from typing import Optional + +from ui_tests.test_application import TestApplication + + +class Declarations: + app: Optional[TestApplication] = None diff --git a/kdb-bot/test/ui_tests_shared/decorators.py b/kdb-bot/test/ui_tests_shared/decorators.py new file mode 100644 index 00000000..f4530baa --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/decorators.py @@ -0,0 +1,9 @@ +import asyncio + + +def async_test(coro): + def wrapper(*args, **kwargs): + loop = asyncio.get_running_loop() + return loop.run_until_complete(coro(*args, **kwargs)) + + return wrapper diff --git a/kdb-bot/test/ui_tests_shared/test_case_with_app.py b/kdb-bot/test/ui_tests_shared/test_case_with_app.py new file mode 100644 index 00000000..d658a483 --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/test_case_with_app.py @@ -0,0 +1,61 @@ +import time +import unittest +from typing import Optional + +from cpl_core.configuration import ConfigurationABC +from cpl_core.dependency_injection import ServiceProviderABC +from selenium import webdriver +from selenium.webdriver import Keys +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.wait import WebDriverWait + +from ui_tests.main import get_app +from ui_tests_shared.declarations import Declarations + + +class TestCaseWithApp(unittest.TestCase): + options = webdriver.ChromeOptions() + options.add_experimental_option('useAutomationExtension', False) + options.add_experimental_option("excludeSwitches", ["enable-automation"]) + + _config: Optional[ConfigurationABC] = None + _services: Optional[ServiceProviderABC] = None + _driver = webdriver.Chrome(options=options) + + _is_logged_in = False + + @classmethod + def setUpClass(cls): + if Declarations.app is None: + get_app() + + cls._config = Declarations.app.config + cls._services = Declarations.app.services + cls._login() + + @classmethod + def _login(cls): + if cls._is_logged_in: + return + + # use full xpath: https://stackoverflow.com/questions/71179006/how-can-selenium-python-chrome-find-web-elements-visible-in-dev-tools-but-no + + cls._driver.get('https://discord.com/login') + + WebDriverWait(cls._driver, 20).until(expected_conditions.presence_of_element_located((By.NAME, 'email'))) + + mail = cls._driver.find_element(By.NAME, 'email') + mail.clear() + mail.send_keys("dev.sven.heidemann@sh-edraft.de") + mail.send_keys(Keys.RETURN) + + pw = cls._driver.find_element(By.NAME, 'password') + pw.clear() + pw.send_keys("Heidemann1410") + pw.send_keys(Keys.RETURN) + time.sleep(1) + pw.send_keys(Keys.RETURN) + + WebDriverWait(cls._driver, 20).until(expected_conditions.url_matches('https://discord.com/channels/@me')) + time.sleep(4) diff --git a/kdb-bot/test/ui_tests_shared/ui-tests-shared.json b/kdb-bot/test/ui_tests_shared/ui-tests-shared.json new file mode 100644 index 00000000..bf1f15ab --- /dev/null +++ b/kdb-bot/test/ui_tests_shared/ui-tests-shared.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "ui-tests-shared", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.10.0.post9" + ], + "DevDependencies": [ + "cpl-cli>=2022.10.0" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "console", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "ui_tests_shared.main", + "EntryPoint": "ui-tests-shared", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file diff --git a/kdb-bot/test/ui_tests_tests/__init__.py b/kdb-bot/test/ui_tests_tests/__init__.py new file mode 100644 index 00000000..ad5eca30 --- /dev/null +++ b/kdb-bot/test/ui_tests_tests/__init__.py @@ -0,0 +1 @@ +# imports: diff --git a/kdb-bot/test/ui_tests_tests/appsettings.json b/kdb-bot/test/ui_tests_tests/appsettings.json new file mode 100644 index 00000000..629e6ebd --- /dev/null +++ b/kdb-bot/test/ui_tests_tests/appsettings.json @@ -0,0 +1,15 @@ +{ + "TimeFormatSettings": { + "DateFormat": "%Y-%m-%d", + "TimeFormat": "%H:%M:%S", + "DateTimeFormat": "%Y-%m-%d %H:%M:%S.%f", + "DateTimeLogFormat": "%Y-%m-%d_%H-%M-%S" + }, + + "LoggingSettings": { + "Path": "logs/", + "Filename": "log_$start_time.log", + "ConsoleLogLevel": "ERROR", + "FileLogLevel": "WARN" + } +} diff --git a/kdb-bot/test/ui_tests_tests/cases/__init__.py b/kdb-bot/test/ui_tests_tests/cases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kdb-bot/test/ui_tests_tests/cases/ping_test_case.py b/kdb-bot/test/ui_tests_tests/cases/ping_test_case.py new file mode 100644 index 00000000..349ab698 --- /dev/null +++ b/kdb-bot/test/ui_tests_tests/cases/ping_test_case.py @@ -0,0 +1,22 @@ +import discord + +from ui_tests_shared.command_test_case_with_app import CommandTestCaseWithApp +from ui_tests_shared.decorators import async_test + + +class PingTestCase(CommandTestCaseWithApp): + + def setUp(self): + pass + + @async_test + async def test_ping(self): + correct_response = self._t.transform('modules.base.pong') + self.assertIsNotNone(correct_response) + self.send_command('ping') + + def check(m: discord.Message): + return m.content == correct_response and m.author.id == 998159802393964594 + + response = await self._bot.wait_for('message', check=check) + self.assertEqual(response.content, correct_response) diff --git a/kdb-bot/test/ui_tests_tests/ui-tests-tests.json b/kdb-bot/test/ui_tests_tests/ui-tests-tests.json new file mode 100644 index 00000000..d8b41933 --- /dev/null +++ b/kdb-bot/test/ui_tests_tests/ui-tests-tests.json @@ -0,0 +1,46 @@ +{ + "ProjectSettings": { + "Name": "ui-tests-tests", + "Version": { + "Major": "0", + "Minor": "0", + "Micro": "0" + }, + "Author": "", + "AuthorEmail": "", + "Description": "", + "LongDescription": "", + "URL": "", + "CopyrightDate": "", + "CopyrightName": "", + "LicenseName": "", + "LicenseDescription": "", + "Dependencies": [ + "cpl-core>=2022.10.0.post9" + ], + "DevDependencies": [ + "cpl-cli>=2022.10.0" + ], + "PythonVersion": ">=3.10.4", + "PythonPath": { + "linux": "" + }, + "Classifiers": [] + }, + "BuildSettings": { + "ProjectType": "console", + "SourcePath": "", + "OutputPath": "../../dist", + "Main": "ui_tests__tests.main", + "EntryPoint": "ui-tests_tests", + "IncludePackageData": false, + "Included": [], + "Excluded": [ + "*/__pycache__", + "*/logs", + "*/tests" + ], + "PackageData": {}, + "ProjectReferences": [] + } +} \ No newline at end of file