Added test settings & improved test architecture #139
This commit is contained in:
		| @@ -1,2 +1,14 @@ | ||||
| # kd_discord_bot | ||||
|  | ||||
| ## Test Bot | ||||
|  | ||||
| To test the bot run unittests or call ```cpl test```. | ||||
| Configure test instance by creating the file ./test/ui_tests/.env and set following environment variables: | ||||
|  | ||||
| ```sh | ||||
| KDB_TEST_DB_PASSWORD= | ||||
| KDB_TEST_NAME= | ||||
| KDB_TEST_TOKEN= | ||||
| KDB_TEST_DISCORD_MAIL= | ||||
| KDB_TEST_DISCORD_PASSWORD= | ||||
| ``` | ||||
| @@ -15,14 +15,14 @@ | ||||
|       "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-shared": "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", | ||||
|       "test": "export $(cat test/ui_tests/.env); export PYTHONPATH=$PWD/src:$PYTHONPATH; cpl run ui-tests", | ||||
|       "sv": "cpl set-version $ARGS", | ||||
|       "set-version": "cpl run set-version $ARGS; echo '';", | ||||
|       "gv": "cpl get-version", | ||||
|   | ||||
| @@ -16,5 +16,10 @@ | ||||
|     "Languages": [ | ||||
|       "de" | ||||
|     ] | ||||
|   }, | ||||
|   "TestSettings": { | ||||
|     "LoginUrl": "https://discord.com/login", | ||||
|     "MePageUrl": "https://discord.com/channels/@me", | ||||
|     "CmdURL": "https://discord.com/channels/910199451145076828/911578636899987526" | ||||
|   } | ||||
| } | ||||
| @@ -3,7 +3,7 @@ import asyncio | ||||
| from cpl_core.application import ApplicationABC | ||||
| from cpl_core.application import ApplicationBuilder | ||||
|  | ||||
| from bot.startup_test_extension import StartupTestExtension | ||||
| from startup_test_extension import StartupTestExtension | ||||
| from ui_tests.startup import Startup | ||||
| from ui_tests_shared.declarations import Declarations | ||||
|  | ||||
|   | ||||
| @@ -21,7 +21,7 @@ class Startup(StartupABC): | ||||
|         self._config: Optional[ConfigurationABC] = None | ||||
|  | ||||
|     def configure_configuration(self, configuration: ConfigurationABC, environment: ApplicationEnvironment) -> ConfigurationABC: | ||||
|         configuration.add_environment_variables('KDB-TEST_') | ||||
|         configuration.add_environment_variables('KDB_TEST_') | ||||
|         configuration.add_environment_variables('DISCORD_') | ||||
|  | ||||
|         cwd = os.path.dirname(os.path.realpath(__file__)) | ||||
|   | ||||
| @@ -1,19 +1,10 @@ | ||||
| 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): | ||||
| 
 | ||||
| @@ -23,6 +14,7 @@ class StartupTestExtension(StartupExtensionABC): | ||||
|     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__))) | ||||
|         environment.set_working_directory('../../src/bot/') | ||||
| 
 | ||||
|     def configure_services(self, services: ServiceCollectionABC, env: ApplicationEnvironmentABC): | ||||
|         pass | ||||
| @@ -0,0 +1,44 @@ | ||||
| { | ||||
|   "ProjectSettings": { | ||||
|     "Name": "ui-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": {}, | ||||
|     "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": [] | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| import time | ||||
| from typing import Optional | ||||
|  | ||||
| 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 | ||||
| @@ -11,13 +11,15 @@ from selenium.webdriver.support import expected_conditions | ||||
| from selenium.webdriver.support.wait import WebDriverWait | ||||
|  | ||||
| from ui_tests_shared.test_case_with_app import TestCaseWithApp | ||||
| from ui_tests_shared.ui import UI | ||||
|  | ||||
|  | ||||
| class CommandTestCaseWithApp(TestCaseWithApp): | ||||
|     _cmd: WebElement | ||||
|  | ||||
|     _bot = None | ||||
|     _t = None | ||||
|     _bot: Optional[DiscordBotServiceABC] = None | ||||
|     _t: Optional[TranslatePipe] = None | ||||
|  | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
| @@ -25,22 +27,22 @@ class CommandTestCaseWithApp(TestCaseWithApp): | ||||
|         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') | ||||
|     @classmethod | ||||
|     def send_command(cls, cmd: str): | ||||
|         UI.driver.get(cls._test_settings.cmd_url) | ||||
|         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 = UI.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( | ||||
|         WebDriverWait(UI.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) | ||||
|         UI.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) | ||||
|   | ||||
							
								
								
									
										1
									
								
								kdb-bot/test/ui_tests_shared/configuration/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								kdb-bot/test/ui_tests_shared/configuration/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| # imports | ||||
							
								
								
									
										35
									
								
								kdb-bot/test/ui_tests_shared/configuration/test_settings.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								kdb-bot/test/ui_tests_shared/configuration/test_settings.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import traceback | ||||
|  | ||||
| from cpl_core.configuration.configuration_model_abc import ConfigurationModelABC | ||||
| from cpl_core.console import Console | ||||
|  | ||||
|  | ||||
| class TestSettings(ConfigurationModelABC): | ||||
|  | ||||
|     def __init__(self): | ||||
|         ConfigurationModelABC.__init__(self) | ||||
|  | ||||
|         self._login_url = '' | ||||
|         self._me_page_url = '' | ||||
|         self._cmd_url = '' | ||||
|  | ||||
|     @property | ||||
|     def login_url(self) -> str: | ||||
|         return self._login_url | ||||
|  | ||||
|     @property | ||||
|     def me_page_url(self) -> str: | ||||
|         return self._me_page_url | ||||
|  | ||||
|     @property | ||||
|     def cmd_url(self) -> str: | ||||
|         return self._cmd_url | ||||
|  | ||||
|     def from_dict(self, settings: dict): | ||||
|         try: | ||||
|             self._login_url = settings['LoginUrl'] | ||||
|             self._me_page_url = settings['MePageUrl'] | ||||
|             self._cmd_url = settings['CmdURL'] | ||||
|         except Exception as e: | ||||
|             Console.error(f'[ ERROR ] [ {__name__} ]: Reading error in {type(self).__name__} settings') | ||||
|             Console.error(f'[ EXCEPTION ] [ {__name__} ]: {e} -> {traceback.format_exc()}') | ||||
| @@ -1,7 +1,12 @@ | ||||
| from typing import Optional | ||||
|  | ||||
| from cpl_core.environment import ApplicationEnvironmentABC | ||||
|  | ||||
| from ui_tests.test_application import TestApplication | ||||
| from ui_tests_shared.configuration.test_settings import TestSettings | ||||
|  | ||||
|  | ||||
| class Declarations: | ||||
|     app: Optional[TestApplication] = None | ||||
|     app: Optional[TestApplication] | ||||
|     env: Optional[ApplicationEnvironmentABC] | ||||
|     test_settings: Optional[TestSettings] | ||||
|   | ||||
| @@ -1,29 +1,19 @@ | ||||
| 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.configuration.test_settings import TestSettings | ||||
| from ui_tests_shared.declarations import Declarations | ||||
| from ui_tests_shared.ui import UI | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     _test_settings: Optional[TestSettings] = None | ||||
|  | ||||
|     @classmethod | ||||
|     def setUpClass(cls): | ||||
| @@ -32,30 +22,6 @@ class TestCaseWithApp(unittest.TestCase): | ||||
|  | ||||
|         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) | ||||
|         cls._test_settings: TestSettings = cls._config.get_configuration(TestSettings) | ||||
|         UI.set_settings(cls._test_settings) | ||||
|         UI.login(cls._config.get_configuration('DISCORD_MAIL'), cls._config.get_configuration('DISCORD_PASSWORD')) | ||||
|   | ||||
| @@ -22,9 +22,7 @@ | ||||
|       "cpl-cli>=2022.10.0" | ||||
|     ], | ||||
|     "PythonVersion": ">=3.10.4", | ||||
|     "PythonPath": { | ||||
|       "linux": "" | ||||
|     }, | ||||
|     "PythonPath": {}, | ||||
|     "Classifiers": [] | ||||
|   }, | ||||
|   "BuildSettings": { | ||||
|   | ||||
							
								
								
									
										56
									
								
								kdb-bot/test/ui_tests_shared/ui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								kdb-bot/test/ui_tests_shared/ui.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| 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.configuration.test_settings import TestSettings | ||||
| from ui_tests_shared.declarations import Declarations | ||||
|  | ||||
|  | ||||
| class UI: | ||||
|     _test_settings: Optional[TestSettings] = None | ||||
|  | ||||
|     options = webdriver.ChromeOptions() | ||||
|     options.add_experimental_option('useAutomationExtension', False) | ||||
|     options.add_experimental_option("excludeSwitches", ["enable-automation"]) | ||||
|     driver = webdriver.Chrome(options=options) | ||||
|  | ||||
|     _is_logged_in = False | ||||
|  | ||||
|     @classmethod | ||||
|     def set_settings(cls, test_settings: TestSettings): | ||||
|         cls._test_settings = test_settings | ||||
|  | ||||
|     @classmethod | ||||
|     def login(cls, mail: str, password: str): | ||||
|         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(cls._test_settings.login_url) | ||||
|  | ||||
|         WebDriverWait(cls.driver, 20).until(expected_conditions.presence_of_element_located((By.NAME, 'email'))) | ||||
|  | ||||
|         mail_element = cls.driver.find_element(By.NAME, 'email') | ||||
|         mail_element.clear() | ||||
|         mail_element.send_keys(mail) | ||||
|         mail_element.send_keys(Keys.RETURN) | ||||
|  | ||||
|         pw_element = cls.driver.find_element(By.NAME, 'password') | ||||
|         pw_element.clear() | ||||
|         pw_element.send_keys(password) | ||||
|         pw_element.send_keys(Keys.RETURN) | ||||
|         time.sleep(1) | ||||
|         pw_element.send_keys(Keys.RETURN) | ||||
|  | ||||
|         WebDriverWait(cls.driver, 20).until(expected_conditions.url_matches(cls._test_settings.me_page_url)) | ||||
|         time.sleep(4) | ||||
| @@ -1,3 +1,5 @@ | ||||
| import unittest | ||||
|  | ||||
| import discord | ||||
|  | ||||
| from ui_tests_shared.command_test_case_with_app import CommandTestCaseWithApp | ||||
| @@ -6,9 +8,6 @@ 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') | ||||
|   | ||||
| @@ -22,9 +22,7 @@ | ||||
|       "cpl-cli>=2022.10.0" | ||||
|     ], | ||||
|     "PythonVersion": ">=3.10.4", | ||||
|     "PythonPath": { | ||||
|       "linux": "" | ||||
|     }, | ||||
|     "PythonPath": {}, | ||||
|     "Classifiers": [] | ||||
|   }, | ||||
|   "BuildSettings": { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user