diff --git a/README.md b/README.md index 54fd681..159e16e 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,19 @@ Pytest-Abra is an installable python package design to test instances created with [abra](https://docs.coopcloud.tech/abra/). After installation, you will have two things: -- `abratest` CLI command +- `abratest` CLI command. *Used to initialize the testing.* -- `pytest-abra` Pytest plugin +- `pytest-abra` Pytest plugin. *Automatically loads custom fixtures in any pytest (see `pytest_abra/custom_fixtures.py`)* -## CLI (abratest) +## CLI (`abratest`) -The easiest way to call abratest is via the helper script in `main.py`. You can also directly call abratest via terminal, but you will have to make sure that the requirements below are met. To do that, you can call `abratest` with: +`abratest` can be called via terminal: ```bash abratest [arguments] ``` -The cli command abratest has 3 **required arguments**: +To run successfully, very specific arguments are required. The easiest way to use abratest is with the helper script in `main.py`. Of yourse you can implement a similar helper script in the language of your liking. The cli command `abratest` has 3 **required arguments**: - `--env_paths`: list of the .env files used in the test - `--recipes_dir`: directory of all available abra recipes @@ -66,12 +66,20 @@ DIR recipes_dir [contains abra recipes] └── [pytest_files] ``` -The class `RunnerWordpress` will be automatically imported by `importlib`, which is equivalent to +The class `RunnerWordpress` will be automatically imported using `importlib` library, which is equivalent to the code below. Note that `recipes_dir` will be added to sys.path automatically for the import to work and that every `Runner` class matching `recipes_dir.rglob("*/runner*.py")` will be imported. ```python from wordpress.tests_wordpress.runner_wordpress import RunnerWordpress ``` +### output_dir [string] + +Path to the directory where all test outputs are stored (test report, tracebacks, playwright traces etc.) + +``` +abratest --output_dir /path/to/output +``` + # Usage To use pytest-abra, follow these steps: @@ -108,7 +116,7 @@ Run the script with python main.py ``` -# 2.2 Run with Docker +## 2.2 Run with Docker ```bash docker compose build # build the image @@ -128,12 +136,13 @@ Force rebuild without cache docker-compose build --no-cache ``` -## Codegen +## Playwright Debug & Codegen -Use playwright codegen to create code for new testes easily https://playwright.dev/python/docs/codegen +Use playwright debug mode or codegen to create testing code easily by recording browser actions https://playwright.dev/python/docs/codegen ```bash -playwright codegen demo.playwright.dev/todomvc +abratest --debug # launch your tests in debug mode +playwright codegen demo.playwright.dev/todomvc # visit given url in codegen mode ``` ## Development diff --git a/previous-work/wordpress_test.py b/previous-work/wordpress_test.py index 9f8173f..960fa72 100644 --- a/previous-work/wordpress_test.py +++ b/previous-work/wordpress_test.py @@ -3,12 +3,12 @@ from playwright.sync_api import BrowserContext, expect from pytest_abra.dir_manager import DirManager -def test_wordpress(admin_session: BrowserContext, dotenv_config: dict[str, str], DIR: DirManager): +def test_wordpress(admin_session: BrowserContext, env_config: dict[str, str], DIR: DirManager): page_authentik = admin_session.new_page() with page_authentik.expect_popup() as event_context: page_authentik.get_by_role("link", name="Wordpress").click() page_wordpress = event_context.value expect(page_wordpress.locator("#wpcontent")).to_be_visible() - if "locale" in dotenv_config and "de" in dotenv_config["locale"]: + if "locale" in env_config and "de" in env_config["locale"]: expect(page_wordpress.get_by_role("heading")).to_have_text("Willkommen bei WordPress!") diff --git a/prototyping/__init__.py b/prototyping/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/prototyping/dependency_injection.py b/prototyping/dependency_injection.py new file mode 100644 index 0000000..dbd3760 --- /dev/null +++ b/prototyping/dependency_injection.py @@ -0,0 +1,19 @@ +import inspect + +a = 2 +b = 3 +c = 4 + + +def func(a: int, c: int) -> int: + return a + c + + +arg_names = inspect.getfullargspec(func).args +print(arg_names) # ['a', 'c'] + +arguments = {arg: globals()[arg] for arg in arg_names if arg in globals()} +print(arguments) # {'a': 2, 'c': 4} + +result = func(**arguments) +print(result) # 6 diff --git a/prototyping/email_stuff_2.py b/prototyping/email_imbox.py similarity index 100% rename from prototyping/email_stuff_2.py rename to prototyping/email_imbox.py diff --git a/prototyping/email_stuff.py b/prototyping/email_stuff.py deleted file mode 100644 index ce0d850..0000000 --- a/prototyping/email_stuff.py +++ /dev/null @@ -1,49 +0,0 @@ -# %% -import email -import json -import os -from email.header import decode_header -from imaplib import IMAP4, IMAP4_SSL -from pathlib import Path - -# -------------------------------- credentials ------------------------------- # - -cred_file = Path("../credentials.json") -with open(cred_file, "r") as f: - CREDENTIALS = json.load(f) - -for key, value in CREDENTIALS.items(): - os.environ[key] = value - -IMAP_HOST = os.environ["IMAP_HOST"] -IMAP_PORT = os.environ["IMAP_PORT"] -IMAP_USER = os.environ["IMAP_USER"] -IMAP_PASS = os.environ["IMAP_PASS"] - - -# ----------------------------------- imap ----------------------------------- # - - -with IMAP4_SSL(host=IMAP_HOST) as imap_server: - imap_server.login(IMAP_USER, IMAP_PASS) - imap_server.select("INBOX") - - # Search for all emails in the folder - status, email_ids = imap_server.search(None, "ALL") - email_ids = email_ids[0].split() - - # Fetch email details using the retrieved IDs - for email_id in email_ids: - result, data = imap_server.fetch(email_id, "(RFC822)") - raw_email = data[0][1] # Raw content of the email - email_message = email.message_from_bytes(raw_email) - - # Extract the subject - subject_encoded = email_message.get("Subject") - decoded_subject = decode_header(subject_encoded)[0][0] - - if isinstance(decoded_subject, bytes): - decoded_subject = decoded_subject.decode() - - # Print or use the subject as needed - print("Subject:", decoded_subject) diff --git a/prototyping/structure.md b/prototyping/structure.md new file mode 100644 index 0000000..15c820f --- /dev/null +++ b/prototyping/structure.md @@ -0,0 +1,80 @@ + + +Abratest has 3 required inputs, but most importantly the test configuration is done through the .env files given with the --env_paths argument. So let's say we want to run abratest with these 3 .env files: + +- config1.env [of TYPE authentik] + +- config2.env [of TYPE wordpress] + +- config3.env [of TYPE wordpress] + +Now we run + +```bash +abratest --env_paths path/config1.env;path/config2.env;path/config3.env [...other args] +``` + + +``` +abratest -> create Coordinator() instance +└── Coordinator() -> create Runner() subclass instances + ├── RunnerAuthentik() [based on config1.env, loaded + │ │ from abra/recipes/authentik] + │ │ # RunnerAuthentik with 3 test files: + │ ├── RUN pytest path/setup_authentik.py + │ ├── RUN pytest path/test_authentik_1.py + │ └── RUN pytest path/test_authentik_2.py + ├── RunnerWordpress() [based on config2.env, loaded + │ │ from abra/recipes/wordpress] + │ │ # RunnerWordpress with 1 test file + │ ├── RUN pytest path/setup_authentik.py + │ ├── RUN pytest path/test_authentik_1.py + │ └── RUN pytest path/test_authentik_2.py + └── RunnerWordpress() [based on config3.env, loaded + │ from abra/recipes/wordpress] + │ # RunnerWordpress with 1 test file + ├── RUN pytest path/setup_authentik.py + ├── RUN pytest path/test_authentik_1.py + └── RUN pytest path/test_authentik_2.py + + +``` + +Coordinator will take care of the correct order of the tests. In general, tests are placed in one of 3 categories: `setups`, `tests` and `cleanups`. To associate a test with one of these categories, place the Test in the corresponding list of the Runner class, i.e. Runner.setups = [test] or Runner.tests = [test]. The execution order will be. + +> [setups] ➔ [tests] ➔ [cleanups] + + +Furthermore, some `Runner` classes can depend on others. For example, `RunnerWordpress` depends on `RunnerAuthentik`. Therefore, `Coordinator` will make sure that `RunnerAuthentik` runs before `RunnerWordpress`. We will end up with with this order: + +| # | Runner | Type | +| --- | -------------- | -------- | +| 1. | Authentik | setups | +| 2. | Wordpress-1 | setups | +| 3. | Wordpress-2 | setups | +| 4. | Authentik | tests | +| 5. | Wordpress-1 | tests | +| 6. | Wordpress-2 | tests | +| 7. | Authentik | cleanups | +| 8. | Wordpress-1 | cleanups | +| 9. | Wordpress-2 | cleanups | + + + +To comprehend this process, let's examine a simplified rendition of the `RunnerWordpress` class. Within it, there exist two setup scripts and two test scripts, one of which operates conditionally. + + +```python +class RunnerWordpress(Runner): + env_type = "wordpress" + dependencies = ["authentik"] + setups = [ + Test(test_file="setup_wordpress_1.py"), + Test(test_file="setup_wordpress_2.py"), + ] + tests = [ + Test(test_file="test_wordpress.py"), + Test(condition=condition_function, test_file="test_wordpress_conditional.py"), + ] + cleanups = [] +``` diff --git a/pyproject.toml b/pyproject.toml index 20d9b07..9bf9361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ ] [project.entry-points.pytest11] -pytest_abra = "pytest_abra.pytest_abra" +pytest_abra = "pytest_abra.custom_fixtures" [project.scripts] abratest = "pytest_abra.cli:run" diff --git a/pytest_abra/__init__.py b/pytest_abra/__init__.py index e69de29..18988b8 100644 --- a/pytest_abra/__init__.py +++ b/pytest_abra/__init__.py @@ -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", +] diff --git a/pytest_abra/cli.py b/pytest_abra/cli.py index 8658dbb..1ebf852 100644 --- a/pytest_abra/cli.py +++ b/pytest_abra/cli.py @@ -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, diff --git a/pytest_abra/coordinator.py b/pytest_abra/coordinator.py index 66e953a..86cecdf 100644 --- a/pytest_abra/coordinator.py +++ b/pytest_abra/coordinator.py @@ -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 diff --git a/pytest_abra/pytest_abra.py b/pytest_abra/custom_fixtures.py similarity index 56% rename from pytest_abra/pytest_abra.py rename to pytest_abra/custom_fixtures.py index 33a74ae..2c343a9 100644 --- a/pytest_abra/pytest_abra.py +++ b/pytest_abra/custom_fixtures.py @@ -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 diff --git a/pytest_abra/demo.py b/pytest_abra/demo.py deleted file mode 100644 index 0ece3c2..0000000 --- a/pytest_abra/demo.py +++ /dev/null @@ -1 +0,0 @@ -print("wooooorking") diff --git a/pytest_abra/dir_manager.py b/pytest_abra/dir_manager.py index fb29e4d..1ff4c11 100644 --- a/pytest_abra/dir_manager.py +++ b/pytest_abra/dir_manager.py @@ -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 ... """ diff --git a/pytest_abra/env_manager.py b/pytest_abra/env_manager.py index f6f16b5..f9d79cd 100644 --- a/pytest_abra/env_manager.py +++ b/pytest_abra/env_manager.py @@ -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 -- 00-authentik-login.test.dev.local-it.cloud.env""" diff --git a/pytest_abra/runner.py b/pytest_abra/runner.py index d17b756..1a75d27 100644 --- a/pytest_abra/runner.py +++ b/pytest_abra/runner.py @@ -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 diff --git a/pytest_abra/utils.py b/pytest_abra/utils.py index b820416..828e6b9 100644 --- a/pytest_abra/utils.py +++ b/pytest_abra/utils.py @@ -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 diff --git a/recipes/authentik/tests_authentik/runner_authentik.py b/recipes/authentik/tests_authentik/runner_authentik.py index fb8c3b2..0570db7 100644 --- a/recipes/authentik/tests_authentik/runner_authentik.py +++ b/recipes/authentik/tests_authentik/runner_authentik.py @@ -1,12 +1,4 @@ -from pytest_abra.runner import Runner, Test - - -def condition_always_true(dotenv_config: dict[str, str]) -> bool: - return True - - -def condition_always_false(dotenv_config: dict[str, str]) -> bool: - return False +from pytest_abra import Runner, Test class RunnerAuthentik(Runner): diff --git a/recipes/authentik/tests_authentik/setup_authentik.py b/recipes/authentik/tests_authentik/setup_authentik.py index 680a85d..9aa6e3b 100644 --- a/recipes/authentik/tests_authentik/setup_authentik.py +++ b/recipes/authentik/tests_authentik/setup_authentik.py @@ -14,20 +14,20 @@ ADMIN_PASS = os.environ["ADMIN_PASS"] TESTUSER = {"username": "testuser", "name": "Test User", "password": "test123", "email": "test@example.com"} -def setup_admin_state(context: BrowserContext, dotenv_config: dict[str, str], DIR: DirManager): +def setup_admin_state(context: BrowserContext, env_config: dict[str, str], DIR: DirManager): # go to page page = context.new_page() - url = "https://" + dotenv_config["DOMAIN"] + url = "https://" + env_config["DOMAIN"] page.goto(url) # check welcome message - welcome_message = dotenv_config.get("welcome_message") + welcome_message = env_config.get("welcome_message") if welcome_message: expect(page.get_by_text(welcome_message)).to_be_visible() # login - page.locator('input[name="uidField"]').fill(ADMIN_USER) - page.locator('ak-stage-identification input[name="password"]').fill(ADMIN_PASS) + page.locator("input[name='uidField']").fill(ADMIN_USER) + page.locator("ak-stage-identification input[name='password']").fill(ADMIN_PASS) page.get_by_role("button", name="Log In").click() expect(page.locator("ak-library")).to_be_visible() @@ -35,7 +35,7 @@ def setup_admin_state(context: BrowserContext, dotenv_config: dict[str, str], DI context.storage_state(path=DIR.STATES / "authentik_admin_state.json") -def check_if_user_exists(admin_context: BrowserContext, dotenv_config: dict[str, str], URL: BaseUrl): +def check_if_user_exists(admin_context: BrowserContext, env_config: dict[str, str], URL: BaseUrl): # go to admin page page = admin_context.new_page() page.goto(URL.get()) @@ -49,7 +49,7 @@ def check_if_user_exists(admin_context: BrowserContext, dotenv_config: dict[str, return user.is_visible() -def create_invite_link(admin_context: BrowserContext, dotenv_config: dict[str, str], URL: BaseUrl): +def create_invite_link(admin_context: BrowserContext, env_config: dict[str, str], URL: BaseUrl): # go to admin page page = admin_context.new_page() page.goto(URL.get()) @@ -98,19 +98,19 @@ def create_user(user_context: BrowserContext, invitelink): expect(page.locator("ak-library")).to_be_visible() -def setup_user_state(context: BrowserContext, dotenv_config: dict[str, str], DIR: DirManager, URL: BaseUrl): +def setup_user_state(context: BrowserContext, env_config: dict[str, str], DIR: DirManager, URL: BaseUrl): # load admin cookies to context state_file = DIR.STATES / "authentik_admin_state.json" storage_state = json.loads(state_file.read_bytes()) context.add_cookies(storage_state["cookies"]) - if check_if_user_exists(context, dotenv_config, URL): + if check_if_user_exists(context, env_config, URL): # just login with user pass context.clear_cookies() else: # get invite_link - invite_link = create_invite_link(context, dotenv_config, URL) + invite_link = create_invite_link(context, env_config, URL) # create user context.clear_cookies() create_user(context, invite_link) diff --git a/recipes/nextcloud/tests_nextcloud/runner_nextcloud.py b/recipes/nextcloud/tests_nextcloud/runner_nextcloud.py index 79b5050..0cb466c 100644 --- a/recipes/nextcloud/tests_nextcloud/runner_nextcloud.py +++ b/recipes/nextcloud/tests_nextcloud/runner_nextcloud.py @@ -1,8 +1,4 @@ -from pytest_abra.runner import Runner, Test - - -def condition_always_false(dotenv_config: dict[str, str]) -> bool: - return False +from pytest_abra import Runner, Test class RunnerNextcloud(Runner): diff --git a/recipes/nextcloud/tests_nextcloud/tests_nextcloud.py b/recipes/nextcloud/tests_nextcloud/tests_nextcloud.py index b79150f..496b3ae 100644 --- a/recipes/nextcloud/tests_nextcloud/tests_nextcloud.py +++ b/recipes/nextcloud/tests_nextcloud/tests_nextcloud.py @@ -4,9 +4,9 @@ import pytest from playwright.sync_api import Page, expect -def test_nextcloud_quota(nextcloud_admin_page: Page, dotenv_config: dict[str, str]): - """Test Nextcloud""" - if dotenv_config.get("DEFAULT_QUOTA"): +def test_nextcloud_quota(nextcloud_admin_page: Page, env_config: dict[str, str]): + """Tests if the quota set in .env file matches the actual quota shown on the page within 10%""" + if env_config.get("DEFAULT_QUOTA"): # get quota from website quota_string = nextcloud_admin_page.get_by_text( re.compile(r"\d*,\d .* \d*,\d") @@ -17,7 +17,7 @@ def test_nextcloud_quota(nextcloud_admin_page: Page, dotenv_config: dict[str, st quota_website = float(out_number) # get quota from env - quota_config_string = dotenv_config["DEFAULT_QUOTA"] # "100 MB" + quota_config_string = env_config["DEFAULT_QUOTA"] # "100 MB" assert "MB" in quota_config_string quota_config = float(quota_config_string.strip("MB")) @@ -27,6 +27,6 @@ def test_nextcloud_quota(nextcloud_admin_page: Page, dotenv_config: dict[str, st @pytest.mark.skip -def test_nextcloud_apps(nextcloud_admin_page: Page, dotenv_config: dict[str, str]): - for app in dotenv_config["nc_apps"]: +def test_nextcloud_apps(nextcloud_admin_page: Page, env_config: dict[str, str]): + for app in env_config["nc_apps"]: expect(nextcloud_admin_page.get_by_role("link", name=app)).to_be_visible() diff --git a/recipes/wordpress/tests_wordpress/conftest.py b/recipes/wordpress/tests_wordpress/conftest.py index 77977a9..69fa4cf 100644 --- a/recipes/wordpress/tests_wordpress/conftest.py +++ b/recipes/wordpress/tests_wordpress/conftest.py @@ -1,10 +1,9 @@ import json import pytest -from dotenv import dotenv_values from playwright.sync_api import BrowserContext, Page -from pytest_abra.dir_manager import DirManager +from pytest_abra import BaseUrl, DirManager pytest_plugins = "authentik.tests_authentik.fixtures_authentik" @@ -18,10 +17,7 @@ def wordpress_admin_context(context: BrowserContext, DIR: DirManager) -> Browser @pytest.fixture -def wordpress_admin_page(wordpress_admin_context: BrowserContext, DIR: DirManager) -> Page: +def wordpress_admin_page(wordpress_admin_context: BrowserContext, URL: BaseUrl) -> Page: page = wordpress_admin_context.new_page() - env_file = DIR.ENV_FILES / "wordpress" - config: dict[str, str] = dotenv_values(env_file) # type: ignore - url = "https://" + config["DOMAIN"] - page.goto(url) + page.goto(URL.get()) return page diff --git a/recipes/wordpress/tests_wordpress/runner_wordpress.py b/recipes/wordpress/tests_wordpress/runner_wordpress.py index eae53ee..d58bd74 100644 --- a/recipes/wordpress/tests_wordpress/runner_wordpress.py +++ b/recipes/wordpress/tests_wordpress/runner_wordpress.py @@ -1,26 +1,21 @@ -from pytest_abra.runner import Runner, Test +from pytest_abra import ConditionArgs, Runner, Test -def condition_always_true(dotenv_config: dict[str, str]) -> bool: - return True - - -def condition_always_false(dotenv_config: dict[str, str]) -> bool: - return False - - -def condition_has_locale(dotenv_config: dict[str, str]) -> bool: - if "LOCALE" in dotenv_config: - if "de" in dotenv_config["LOCALE"]: - return True +def condition_has_locale(args: ConditionArgs) -> bool: + env_config = args.env_config + if "de" in env_config.get("LOCALE", ""): + return True return False class RunnerWordpress(Runner): env_type = "wordpress" dependencies = ["authentik"] - setups = [Test(test_file="setup_wordpress.py")] - tests = [ - Test(test_file="test_wordpress.py"), - Test(condition=condition_has_locale, test_file="test_wordpress_localization.py"), + setups = [ + Test(test_file="setup_wordpress.py"), + Test(test_file="setup_wordpress_trigger_email.py"), + ] + tests = [ + Test(test_file="test_wordpress_receive_email.py", prevent_skip=True), + # Test(condition=condition_has_locale, test_file="test_wordpress_localization.py"), ] diff --git a/recipes/wordpress/tests_wordpress/setup_wordpress.py b/recipes/wordpress/tests_wordpress/setup_wordpress.py index b9fd773..4c44799 100644 --- a/recipes/wordpress/tests_wordpress/setup_wordpress.py +++ b/recipes/wordpress/tests_wordpress/setup_wordpress.py @@ -4,10 +4,10 @@ from playwright.sync_api import BrowserContext, Page, expect from pytest_abra.dir_manager import DirManager -def test_visit_from_domain(authentik_admin_context: BrowserContext, dotenv_config: dict[str, str]): +def test_visit_from_domain(authentik_admin_context: BrowserContext, env_config: dict[str, str]): """visit wordpress directly with admin_session, expect not to be logged in""" page = authentik_admin_context.new_page() - url = "https://" + dotenv_config["DOMAIN"] + url = "https://" + env_config["DOMAIN"] page.goto(url) with pytest.raises(AssertionError): # look for admin bar diff --git a/recipes/wordpress/tests_wordpress/setup_wordpress_trigger_email.py b/recipes/wordpress/tests_wordpress/setup_wordpress_trigger_email.py new file mode 100644 index 0000000..33afb6b --- /dev/null +++ b/recipes/wordpress/tests_wordpress/setup_wordpress_trigger_email.py @@ -0,0 +1,19 @@ +import os + +from playwright.sync_api import Page, expect + +from pytest_abra import BaseUrl + + +def setup_trigger_email(wordpress_admin_page: Page, URL: BaseUrl): + """change profile email to EMAIL to trigger email""" + page = wordpress_admin_page + page.goto(URL.get("wp-admin/profile.php")) + EMAIL = os.environ["IMAP_EMAIL"] + print(EMAIL) + # breakpoint() + page.pause() + page.locator("input[id='email']").fill(EMAIL) + page.locator("input[id='submit']").click() + + expect(page.locator("div.notice").get_by_text(EMAIL)).to_be_visible() diff --git a/recipes/wordpress/tests_wordpress/test_wordpress.py b/recipes/wordpress/tests_wordpress/test_wordpress.py deleted file mode 100644 index e69de29..0000000 diff --git a/recipes/wordpress/tests_wordpress/test_wordpress_localization.py b/recipes/wordpress/tests_wordpress/test_wordpress_localization.py index a0c76ba..51601cb 100644 --- a/recipes/wordpress/tests_wordpress/test_wordpress_localization.py +++ b/recipes/wordpress/tests_wordpress/test_wordpress_localization.py @@ -5,11 +5,11 @@ from playwright.sync_api import BrowserContext, expect from pytest_abra.dir_manager import DirManager -def test_welcome_message(context: BrowserContext, dotenv_config: dict[str, str], DIR: DirManager): +def test_welcome_message(context: BrowserContext, env_config: dict[str, str], DIR: DirManager): page = context.new_page() - url = "https://" + dotenv_config["DOMAIN"] + url = "https://" + env_config["DOMAIN"] page.goto(url) expect(page.locator(".wp-block-heading")).to_be_visible() - if "locale" in dotenv_config and "de" in dotenv_config["locale"]: + if "locale" in env_config and "de" in env_config["locale"]: expect(page.get_by_role("heading")).to_have_text("Willkommen bei WordPress!") diff --git a/recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py b/recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py new file mode 100644 index 0000000..736f18d --- /dev/null +++ b/recipes/wordpress/tests_wordpress/test_wordpress_receive_email.py @@ -0,0 +1,13 @@ +from icecream import ic + +from pytest_abra.custom_fixtures import Message + + +def test_demo(imap_recent_messages: list[Message]): + for message in imap_recent_messages: + print(dir(message)) + ic(message.subject) + ic(message.body["plain"]) + + exit() + assert False diff --git a/tests/test_env_resolution.py b/tests/test_env_resolution.py index 4b11941..ddb5fc4 100644 --- a/tests/test_env_resolution.py +++ b/tests/test_env_resolution.py @@ -20,7 +20,7 @@ def test_complex_sorting() -> None: ] demo_types = ["a", "b", "c", "d", "e", "f", "g"] - env_files = [EnvFile(env_type=t, env_path=Path(), config=dict()) for t in demo_types] + env_files = [EnvFile(env_type=t, env_path=Path(), env_config=dict()) for t in demo_types] EnvManager.sort_env_files_by_rule sorted_env_files = EnvManager.sort_env_files_by_rule(env_files, demo_rules) @@ -36,7 +36,7 @@ def test_circular_import() -> None: ] demo_types = ["a", "b", "c"] - env_files = [EnvFile(env_type=t, env_path=Path(), config=dict()) for t in demo_types] + env_files = [EnvFile(env_type=t, env_path=Path(), env_config=dict()) for t in demo_types] with pytest.raises(ValueError): EnvManager.sort_env_files_by_rule(env_files, demo_rules)