refactoring (#13)

* general project refactoring

* various small improvements

* improve imap fixture with helper functions and typing

* add wordpress send email setup

* add wordpress receive email test

* add various documentation

Reviewed-on: local-it-infrastructure/e2e_tests#13
Co-authored-by: Daniel <d.brummerloh@gmail.com>
Co-committed-by: Daniel <d.brummerloh@gmail.com>
This commit is contained in:
Daniel 2023-12-08 18:17:31 +01:00 committed by dan
parent 41a042f07d
commit d1ff1183a5
29 changed files with 323 additions and 175 deletions

View file

@ -0,0 +1,13 @@
from pytest_abra.coordinator import Coordinator
from pytest_abra.dir_manager import DirManager
from pytest_abra.runner import ConditionArgs, Runner, Test
from pytest_abra.utils import BaseUrl
__all__ = [
"Coordinator",
"ConditionArgs",
"Runner",
"Test",
"DirManager",
"BaseUrl",
]

View file

@ -4,7 +4,7 @@ from pathlib import Path
from loguru import logger
from pytest_abra.coordinator import Coordinator
from pytest_abra import Coordinator
from pytest_abra.dir_manager import DirManager
from pytest_abra.utils import get_datetime_string
@ -18,7 +18,7 @@ def run():
parser.add_argument("--debug", action="store_true", help="Enable Playwright debug mode")
parser.add_argument("--resume", action="store_true", help="Re-run the most recent test, skipping passed tests")
args = parser.parse_args()
ENV_FILES = [Path(s) for s in args.env_paths.split(";")]
env_paths = [Path(s) for s in args.env_paths.split(";")]
# -------------------------- enable playwright debug ------------------------- #
@ -43,7 +43,7 @@ def run():
# ---------------------------- initialize and run ---------------------------- #
coordinator = Coordinator(
env_paths_list=ENV_FILES,
env_paths=env_paths,
output_dir=args.output_dir,
session_id=session_id,
recipes_dir=args.recipes_dir,

View file

@ -15,21 +15,21 @@ from pytest_abra.utils import rmtree
class Coordinator:
def __init__(
self,
env_paths_list: list[Path],
env_paths: list[Path],
output_dir: Path,
session_id: str,
recipes_dir: Path,
timeout: int,
) -> None:
# logging
out_string = "".join([e.name + "\n" for e in env_paths_list])
out_string = "".join([e.name + "\n" for e in env_paths])
out_string += f"output_dir = {output_dir}\n"
out_string += f"session_id = {session_id}"
logger.info(f"initialize Coordinator instance with\nenv_paths_list =\n{out_string}")
self.RUNNER_DICT = self.create_runner_dict(recipes_dir)
self.DIR = DirManager(output_dir=output_dir, session_id=session_id, recipes_dir=recipes_dir)
self.ENV = EnvManager(env_paths_list, self.RUNNER_DICT)
self.ENV = EnvManager(env_paths=env_paths, RUNNER_DICT=self.RUNNER_DICT)
self.TIMEOUT = timeout
def setup_test(self) -> None:
@ -52,7 +52,7 @@ class Coordinator:
"""Creates an instance of the correct Runner class for each given env file"""
runners: list[Runner] = []
for index, env_file in enumerate(env_files):
RunnerClass = self.RUNNER_DICT[env_file.config["TYPE"]]
RunnerClass = self.RUNNER_DICT[env_file.env_config["TYPE"]]
runners.append(RunnerClass(coordinator=self, runner_index=index))
return runners

View file

@ -3,11 +3,14 @@
import os
import re
from imaplib import IMAP4_SSL
from datetime import datetime, timedelta
from pathlib import Path
from typing import Protocol, TypedDict
import pytest
from dotenv import dotenv_values
from icecream import ic
from imbox import Imbox # type: ignore
from playwright.sync_api import BrowserContext, expect
from pytest import Parser
@ -62,39 +65,90 @@ def DIR(request) -> DirManager:
@pytest.fixture(scope="session")
def ENV_FILES(DIR: DirManager) -> dict[int, EnvFile]:
out: dict[int, EnvFile] = dict()
def env_files(DIR: DirManager) -> list[EnvFile]:
"""list of EnvFile objects created from the given env files"""
env_files_dict: dict[int, EnvFile] = dict()
for env_path in DIR.ENV_FILES.glob("*.env"):
config: dict[str, str] = dotenv_values(env_path) # type: ignore
env_type = config["TYPE"]
result = re.search(r"(\d+)-*", env_path.name)
assert result
runner_index = int(result[1])
out[runner_index] = EnvFile(env_path=env_path, config=config, env_type=env_type)
return out
env_files_dict[runner_index] = EnvFile(env_path=env_path, env_config=config, env_type=env_type)
keys = list(env_files_dict.keys())
keys.sort()
return [env_files_dict[key] for key in keys]
@pytest.fixture(scope="session")
def dotenv_config(request, ENV_FILES: dict[int, EnvFile]) -> dict[str, str]:
def env_config(request, env_files: list[EnvFile]) -> dict[str, str]:
"""Current env_config"""
runner_index = request.config.getoption("--runner_index")
return ENV_FILES[runner_index].config
return env_files[runner_index].env_config
@pytest.fixture(scope="session")
def URL(dotenv_config: dict[str, str]) -> BaseUrl:
return BaseUrl(netloc=dotenv_config["DOMAIN"])
def URL(env_config: dict[str, str]) -> BaseUrl:
"""BaseUrl object based on current DOMAIN"""
return BaseUrl(netloc=env_config["DOMAIN"])
@pytest.fixture(scope="session")
def imap_ssl_email_client() -> None:
def imap_client() -> None:
"""imap email client using credentials from environment variables"""
assert os.environ["IMAP_HOST"]
assert os.environ["IMAP_PORT"]
assert os.environ["IMAP_USER"]
assert os.environ["IMAP_PASS"]
port = int(os.environ["IMAP_PORT"])
imap_client = IMAP4_SSL(host=os.environ["IMAP_HOST"], port=port)
imap_client.login(os.environ["IMAP_USER"], os.environ["IMAP_PASS"])
imap_client.select("INBOX")
yield imap_client
imap_client.close()
imap_client.logout()
imbox = Imbox(
hostname=os.environ["IMAP_HOST"],
port=os.environ["IMAP_PORT"],
username=os.environ["IMAP_USER"],
password=os.environ["IMAP_PASS"],
ssl=True,
ssl_context=None,
starttls=False,
)
yield imbox
imbox.logout()
class Body(TypedDict):
plain: list
html: list
class Message(Protocol):
sent_from: list
sent_to: list
subject: str
headers: list
date: str
body: Body
@pytest.fixture
def imap_recent_messages(imap_client: Imbox) -> list[Message]:
"""Get all messages from [n_minutes] ago till now.
# iterate with
for uid, message in messages:
print(uid, message.subject, message.date)"""
N_MINUTES = 30
n_minutes_ago = datetime.now() - timedelta(minutes=N_MINUTES)
uids: list[bytes] = []
messages: list[Message] = []
# for uid, message in imap_client.messages(date__gt=n_minutes_ago):
for uid, message in imap_client.messages():
ic("one time")
uids.append(uid)
messages.append(message)
return messages

View file

@ -1 +0,0 @@
print("wooooorking")

View file

@ -10,11 +10,11 @@ class DirManager:
The structures is as follows:
tests dir/
session_dir-1/
session_id-1/
records
results
states
session_dir-2/
session_id-2/
records
...
"""

View file

@ -1,16 +1,17 @@
import shutil
from pathlib import Path
from typing import NamedTuple
from typing import TYPE_CHECKING, NamedTuple
from dotenv import dotenv_values
from pytest_abra.dir_manager import DirManager
from pytest_abra.runner import Runner
if TYPE_CHECKING:
from pytest_abra.dir_manager import DirManager
from pytest_abra.runner import Runner
class EnvFile(NamedTuple):
env_path: Path
config: dict[str, str]
env_config: dict[str, str]
env_type: str
def __repr__(self) -> str:
@ -23,8 +24,8 @@ class DependencyRule(NamedTuple):
class EnvManager:
def __init__(self, env_paths_list: list[Path], RUNNER_DICT: dict[str, type["Runner"]]):
self.env_files: list[EnvFile] = self._get_env_files(env_paths_list)
def __init__(self, env_paths: list[Path], RUNNER_DICT: dict[str, type["Runner"]]):
self.env_files: list[EnvFile] = self._get_env_files(env_paths)
self.dependency_rules: list[DependencyRule] = self._get_dependency_rules(self.env_files, RUNNER_DICT)
self.env_files = self.sort_env_files_by_rule(self.env_files, self.dependency_rules)
@ -37,7 +38,7 @@ class EnvManager:
config: dict[str, str] = dotenv_values(env_path) # type: ignore
assert "TYPE" in config, f"the env file {env_path} does not specify the required TYPE key."
env_type = config["TYPE"]
env_files.append(EnvFile(env_path=env_path, config=config, env_type=env_type))
env_files.append(EnvFile(env_path=env_path, env_config=config, env_type=env_type))
return env_files
@staticmethod
@ -92,7 +93,7 @@ class EnvManager:
"Could not resolve test order. This is possibly due to a circular dependency (a on b, b on c, c on a)"
)
def copy_env_files(self, DIR: DirManager) -> None:
def copy_env_files(self, DIR: "DirManager") -> None:
"""Copies all env files to STATES/env_files. Files will be renamed to
<index>-<env_type>-<original_name>
00-authentik-login.test.dev.local-it.cloud.env"""

View file

@ -1,6 +1,6 @@
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING, Callable, NamedTuple
import pytest
from loguru import logger
@ -10,10 +10,16 @@ if TYPE_CHECKING:
from pytest_abra.env_manager import EnvFile
class ConditionArgs(NamedTuple):
env_config: dict[str, str]
runner_index: int
env_files: list["EnvFile"]
@dataclass
class Test:
test_file: str
condition: Callable[[dict[str, str]], bool] | None = None
condition: Callable[[ConditionArgs], bool] | None = None
prevent_skip: bool = False
@ -25,17 +31,13 @@ class Runner:
dependencies: list[str] = []
def __init__(self, coordinator: "Coordinator", runner_index: int):
self.coordinator = coordinator # needed?
self.runner_index = runner_index # needed?
self.coordinator = coordinator
self.runner_index = runner_index
self.DIR = coordinator.DIR
self.ENV = coordinator.ENV
self.RUNNER_DICT = coordinator.RUNNER_DICT
self.env_file: EnvFile = self.ENV.env_files[self.runner_index]
self.dotenv_path = self.env_file.env_path
self.config = self.env_file.config
logger.info(f"creating instance of {self.__class__.__name__}")
def run_setups(self):
@ -81,16 +83,28 @@ class Runner:
logger.info(f"skipping {identifier_string}, test has passed")
return
if test.condition and not test.condition(self.config):
# test condition is defined but not met
logger.info(f"skipping {identifier_string}, test condition is not met")
return
if test.condition:
condition_result = self._run_condition(test.condition)
if not condition_result:
# test condition is defined but not met
logger.info(f"skipping {identifier_string}, test condition is not met")
return
# test condition is undefined or not met
logger.info(f"running {identifier_string}")
result = self._call_pytest(full_test_path)
self._create_result_file(result=result, identifier_string=identifier_string)
def _run_condition(self, condition_function: Callable[[ConditionArgs], bool]):
"""run the test condition function with multiple arguments"""
# more arguments can be added later without changing the function signature
conditon_args = ConditionArgs(
env_files=self.ENV.env_files,
runner_index=self.runner_index,
env_config=self.ENV.env_files[self.runner_index].env_config,
)
return condition_function(conditon_args)
def _is_test_passed(self, identifier_string: str, remove_existing: bool = False) -> bool:
"""returns True if the selected test matching identifier_string already passed

View file

@ -6,6 +6,8 @@ from urllib.parse import urlunparse
@dataclass
class BaseUrl:
"""utility class to create a url string with urllib"""
netloc: str
scheme: str = "https"
path: str = ""
@ -33,8 +35,3 @@ def rmtree(root_dir: Path):
child.unlink()
root_dir.rmdir()
def make_url(domain: str) -> str:
"""adds 'http://' at the beginning of a string"""
return "https://" + domain