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

@ -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: 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 ```bash
abratest [arguments] 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 - `--env_paths`: list of the .env files used in the test
- `--recipes_dir`: directory of all available abra recipes - `--recipes_dir`: directory of all available abra recipes
@ -66,12 +66,20 @@ DIR recipes_dir [contains abra recipes]
└── [pytest_files] └── [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 ```python
from wordpress.tests_wordpress.runner_wordpress import RunnerWordpress 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 # Usage
To use pytest-abra, follow these steps: To use pytest-abra, follow these steps:
@ -108,7 +116,7 @@ Run the script with
python main.py python main.py
``` ```
# 2.2 Run with Docker ## 2.2 Run with Docker
```bash ```bash
docker compose build # build the image docker compose build # build the image
@ -128,12 +136,13 @@ Force rebuild without cache
docker-compose build --no-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 ```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 ## Development

View file

@ -3,12 +3,12 @@ from playwright.sync_api import BrowserContext, expect
from pytest_abra.dir_manager import DirManager 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() page_authentik = admin_session.new_page()
with page_authentik.expect_popup() as event_context: with page_authentik.expect_popup() as event_context:
page_authentik.get_by_role("link", name="Wordpress").click() page_authentik.get_by_role("link", name="Wordpress").click()
page_wordpress = event_context.value page_wordpress = event_context.value
expect(page_wordpress.locator("#wpcontent")).to_be_visible() 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!") expect(page_wordpress.get_by_role("heading")).to_have_text("Willkommen bei WordPress!")

View file

@ -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

View file

@ -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)

80
prototyping/structure.md Normal file
View file

@ -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 = []
```

View file

@ -26,7 +26,7 @@ dependencies = [
] ]
[project.entry-points.pytest11] [project.entry-points.pytest11]
pytest_abra = "pytest_abra.pytest_abra" pytest_abra = "pytest_abra.custom_fixtures"
[project.scripts] [project.scripts]
abratest = "pytest_abra.cli:run" abratest = "pytest_abra.cli:run"

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 loguru import logger
from pytest_abra.coordinator import Coordinator from pytest_abra import Coordinator
from pytest_abra.dir_manager import DirManager from pytest_abra.dir_manager import DirManager
from pytest_abra.utils import get_datetime_string 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("--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") parser.add_argument("--resume", action="store_true", help="Re-run the most recent test, skipping passed tests")
args = parser.parse_args() 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 ------------------------- # # -------------------------- enable playwright debug ------------------------- #
@ -43,7 +43,7 @@ def run():
# ---------------------------- initialize and run ---------------------------- # # ---------------------------- initialize and run ---------------------------- #
coordinator = Coordinator( coordinator = Coordinator(
env_paths_list=ENV_FILES, env_paths=env_paths,
output_dir=args.output_dir, output_dir=args.output_dir,
session_id=session_id, session_id=session_id,
recipes_dir=args.recipes_dir, recipes_dir=args.recipes_dir,

View file

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

View file

@ -3,11 +3,14 @@
import os import os
import re import re
from imaplib import IMAP4_SSL from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Protocol, TypedDict
import pytest import pytest
from dotenv import dotenv_values from dotenv import dotenv_values
from icecream import ic
from imbox import Imbox # type: ignore
from playwright.sync_api import BrowserContext, expect from playwright.sync_api import BrowserContext, expect
from pytest import Parser from pytest import Parser
@ -62,39 +65,90 @@ def DIR(request) -> DirManager:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def ENV_FILES(DIR: DirManager) -> dict[int, EnvFile]: def env_files(DIR: DirManager) -> list[EnvFile]:
out: dict[int, EnvFile] = dict() """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"): for env_path in DIR.ENV_FILES.glob("*.env"):
config: dict[str, str] = dotenv_values(env_path) # type: ignore config: dict[str, str] = dotenv_values(env_path) # type: ignore
env_type = config["TYPE"] env_type = config["TYPE"]
result = re.search(r"(\d+)-*", env_path.name) result = re.search(r"(\d+)-*", env_path.name)
assert result assert result
runner_index = int(result[1]) runner_index = int(result[1])
out[runner_index] = EnvFile(env_path=env_path, config=config, env_type=env_type) env_files_dict[runner_index] = EnvFile(env_path=env_path, env_config=config, env_type=env_type)
return out keys = list(env_files_dict.keys())
keys.sort()
return [env_files_dict[key] for key in keys]
@pytest.fixture(scope="session") @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") runner_index = request.config.getoption("--runner_index")
return ENV_FILES[runner_index].config return env_files[runner_index].env_config
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def URL(dotenv_config: dict[str, str]) -> BaseUrl: def URL(env_config: dict[str, str]) -> BaseUrl:
return BaseUrl(netloc=dotenv_config["DOMAIN"]) """BaseUrl object based on current DOMAIN"""
return BaseUrl(netloc=env_config["DOMAIN"])
@pytest.fixture(scope="session") @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_HOST"]
assert os.environ["IMAP_PORT"] assert os.environ["IMAP_PORT"]
assert os.environ["IMAP_USER"] assert os.environ["IMAP_USER"]
assert os.environ["IMAP_PASS"] assert os.environ["IMAP_PASS"]
port = int(os.environ["IMAP_PORT"])
imap_client = IMAP4_SSL(host=os.environ["IMAP_HOST"], port=port) imbox = Imbox(
imap_client.login(os.environ["IMAP_USER"], os.environ["IMAP_PASS"]) hostname=os.environ["IMAP_HOST"],
imap_client.select("INBOX") port=os.environ["IMAP_PORT"],
yield imap_client username=os.environ["IMAP_USER"],
imap_client.close() password=os.environ["IMAP_PASS"],
imap_client.logout() 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: The structures is as follows:
tests dir/ tests dir/
session_dir-1/ session_id-1/
records records
results results
states states
session_dir-2/ session_id-2/
records records
... ...
""" """

View file

@ -1,16 +1,17 @@
import shutil import shutil
from pathlib import Path from pathlib import Path
from typing import NamedTuple from typing import TYPE_CHECKING, NamedTuple
from dotenv import dotenv_values from dotenv import dotenv_values
from pytest_abra.dir_manager import DirManager if TYPE_CHECKING:
from pytest_abra.runner import Runner from pytest_abra.dir_manager import DirManager
from pytest_abra.runner import Runner
class EnvFile(NamedTuple): class EnvFile(NamedTuple):
env_path: Path env_path: Path
config: dict[str, str] env_config: dict[str, str]
env_type: str env_type: str
def __repr__(self) -> str: def __repr__(self) -> str:
@ -23,8 +24,8 @@ class DependencyRule(NamedTuple):
class EnvManager: class EnvManager:
def __init__(self, env_paths_list: list[Path], RUNNER_DICT: dict[str, type["Runner"]]): def __init__(self, env_paths: list[Path], RUNNER_DICT: dict[str, type["Runner"]]):
self.env_files: list[EnvFile] = self._get_env_files(env_paths_list) 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.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) 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 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." assert "TYPE" in config, f"the env file {env_path} does not specify the required TYPE key."
env_type = config["TYPE"] 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 return env_files
@staticmethod @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)" "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 """Copies all env files to STATES/env_files. Files will be renamed to
<index>-<env_type>-<original_name> <index>-<env_type>-<original_name>
00-authentik-login.test.dev.local-it.cloud.env""" 00-authentik-login.test.dev.local-it.cloud.env"""

View file

@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Callable from typing import TYPE_CHECKING, Callable, NamedTuple
import pytest import pytest
from loguru import logger from loguru import logger
@ -10,10 +10,16 @@ if TYPE_CHECKING:
from pytest_abra.env_manager import EnvFile from pytest_abra.env_manager import EnvFile
class ConditionArgs(NamedTuple):
env_config: dict[str, str]
runner_index: int
env_files: list["EnvFile"]
@dataclass @dataclass
class Test: class Test:
test_file: str test_file: str
condition: Callable[[dict[str, str]], bool] | None = None condition: Callable[[ConditionArgs], bool] | None = None
prevent_skip: bool = False prevent_skip: bool = False
@ -25,17 +31,13 @@ class Runner:
dependencies: list[str] = [] dependencies: list[str] = []
def __init__(self, coordinator: "Coordinator", runner_index: int): def __init__(self, coordinator: "Coordinator", runner_index: int):
self.coordinator = coordinator # needed? self.coordinator = coordinator
self.runner_index = runner_index # needed? self.runner_index = runner_index
self.DIR = coordinator.DIR self.DIR = coordinator.DIR
self.ENV = coordinator.ENV self.ENV = coordinator.ENV
self.RUNNER_DICT = coordinator.RUNNER_DICT 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__}") logger.info(f"creating instance of {self.__class__.__name__}")
def run_setups(self): def run_setups(self):
@ -81,7 +83,9 @@ class Runner:
logger.info(f"skipping {identifier_string}, test has passed") logger.info(f"skipping {identifier_string}, test has passed")
return return
if test.condition and not test.condition(self.config): if test.condition:
condition_result = self._run_condition(test.condition)
if not condition_result:
# test condition is defined but not met # test condition is defined but not met
logger.info(f"skipping {identifier_string}, test condition is not met") logger.info(f"skipping {identifier_string}, test condition is not met")
return return
@ -91,6 +95,16 @@ class Runner:
result = self._call_pytest(full_test_path) result = self._call_pytest(full_test_path)
self._create_result_file(result=result, identifier_string=identifier_string) 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: def _is_test_passed(self, identifier_string: str, remove_existing: bool = False) -> bool:
"""returns True if the selected test matching identifier_string already passed """returns True if the selected test matching identifier_string already passed

View file

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

View file

@ -1,12 +1,4 @@
from pytest_abra.runner import Runner, Test from pytest_abra 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
class RunnerAuthentik(Runner): class RunnerAuthentik(Runner):

View file

@ -14,20 +14,20 @@ ADMIN_PASS = os.environ["ADMIN_PASS"]
TESTUSER = {"username": "testuser", "name": "Test User", "password": "test123", "email": "test@example.com"} 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 # go to page
page = context.new_page() page = context.new_page()
url = "https://" + dotenv_config["DOMAIN"] url = "https://" + env_config["DOMAIN"]
page.goto(url) page.goto(url)
# check welcome message # check welcome message
welcome_message = dotenv_config.get("welcome_message") welcome_message = env_config.get("welcome_message")
if welcome_message: if welcome_message:
expect(page.get_by_text(welcome_message)).to_be_visible() expect(page.get_by_text(welcome_message)).to_be_visible()
# login # login
page.locator('input[name="uidField"]').fill(ADMIN_USER) page.locator("input[name='uidField']").fill(ADMIN_USER)
page.locator('ak-stage-identification input[name="password"]').fill(ADMIN_PASS) page.locator("ak-stage-identification input[name='password']").fill(ADMIN_PASS)
page.get_by_role("button", name="Log In").click() page.get_by_role("button", name="Log In").click()
expect(page.locator("ak-library")).to_be_visible() 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") 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 # go to admin page
page = admin_context.new_page() page = admin_context.new_page()
page.goto(URL.get()) page.goto(URL.get())
@ -49,7 +49,7 @@ def check_if_user_exists(admin_context: BrowserContext, dotenv_config: dict[str,
return user.is_visible() 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 # go to admin page
page = admin_context.new_page() page = admin_context.new_page()
page.goto(URL.get()) page.goto(URL.get())
@ -98,19 +98,19 @@ def create_user(user_context: BrowserContext, invitelink):
expect(page.locator("ak-library")).to_be_visible() 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 # load admin cookies to context
state_file = DIR.STATES / "authentik_admin_state.json" state_file = DIR.STATES / "authentik_admin_state.json"
storage_state = json.loads(state_file.read_bytes()) storage_state = json.loads(state_file.read_bytes())
context.add_cookies(storage_state["cookies"]) 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 # just login with user
pass pass
context.clear_cookies() context.clear_cookies()
else: else:
# get invite_link # get invite_link
invite_link = create_invite_link(context, dotenv_config, URL) invite_link = create_invite_link(context, env_config, URL)
# create user # create user
context.clear_cookies() context.clear_cookies()
create_user(context, invite_link) create_user(context, invite_link)

View file

@ -1,8 +1,4 @@
from pytest_abra.runner import Runner, Test from pytest_abra import Runner, Test
def condition_always_false(dotenv_config: dict[str, str]) -> bool:
return False
class RunnerNextcloud(Runner): class RunnerNextcloud(Runner):

View file

@ -4,9 +4,9 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
def test_nextcloud_quota(nextcloud_admin_page: Page, dotenv_config: dict[str, str]): def test_nextcloud_quota(nextcloud_admin_page: Page, env_config: dict[str, str]):
"""Test Nextcloud""" """Tests if the quota set in .env file matches the actual quota shown on the page within 10%"""
if dotenv_config.get("DEFAULT_QUOTA"): if env_config.get("DEFAULT_QUOTA"):
# get quota from website # get quota from website
quota_string = nextcloud_admin_page.get_by_text( quota_string = nextcloud_admin_page.get_by_text(
re.compile(r"\d*,\d .* \d*,\d") 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) quota_website = float(out_number)
# get quota from env # 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 assert "MB" in quota_config_string
quota_config = float(quota_config_string.strip("MB")) 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 @pytest.mark.skip
def test_nextcloud_apps(nextcloud_admin_page: Page, dotenv_config: dict[str, str]): def test_nextcloud_apps(nextcloud_admin_page: Page, env_config: dict[str, str]):
for app in dotenv_config["nc_apps"]: for app in env_config["nc_apps"]:
expect(nextcloud_admin_page.get_by_role("link", name=app)).to_be_visible() expect(nextcloud_admin_page.get_by_role("link", name=app)).to_be_visible()

View file

@ -1,10 +1,9 @@
import json import json
import pytest import pytest
from dotenv import dotenv_values
from playwright.sync_api import BrowserContext, Page 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" pytest_plugins = "authentik.tests_authentik.fixtures_authentik"
@ -18,10 +17,7 @@ def wordpress_admin_context(context: BrowserContext, DIR: DirManager) -> Browser
@pytest.fixture @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() page = wordpress_admin_context.new_page()
env_file = DIR.ENV_FILES / "wordpress" page.goto(URL.get())
config: dict[str, str] = dotenv_values(env_file) # type: ignore
url = "https://" + config["DOMAIN"]
page.goto(url)
return page return page

View file

@ -1,17 +1,9 @@
from pytest_abra.runner import Runner, Test from pytest_abra import ConditionArgs, Runner, Test
def condition_always_true(dotenv_config: dict[str, str]) -> bool: def condition_has_locale(args: ConditionArgs) -> bool:
return True env_config = args.env_config
if "de" in env_config.get("LOCALE", ""):
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 return True
return False return False
@ -19,8 +11,11 @@ def condition_has_locale(dotenv_config: dict[str, str]) -> bool:
class RunnerWordpress(Runner): class RunnerWordpress(Runner):
env_type = "wordpress" env_type = "wordpress"
dependencies = ["authentik"] dependencies = ["authentik"]
setups = [Test(test_file="setup_wordpress.py")] setups = [
tests = [ Test(test_file="setup_wordpress.py"),
Test(test_file="test_wordpress.py"), Test(test_file="setup_wordpress_trigger_email.py"),
Test(condition=condition_has_locale, test_file="test_wordpress_localization.py"), ]
tests = [
Test(test_file="test_wordpress_receive_email.py", prevent_skip=True),
# Test(condition=condition_has_locale, test_file="test_wordpress_localization.py"),
] ]

View file

@ -4,10 +4,10 @@ from playwright.sync_api import BrowserContext, Page, expect
from pytest_abra.dir_manager import DirManager 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""" """visit wordpress directly with admin_session, expect not to be logged in"""
page = authentik_admin_context.new_page() page = authentik_admin_context.new_page()
url = "https://" + dotenv_config["DOMAIN"] url = "https://" + env_config["DOMAIN"]
page.goto(url) page.goto(url)
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
# look for admin bar # look for admin bar

View file

@ -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()

View file

@ -5,11 +5,11 @@ from playwright.sync_api import BrowserContext, expect
from pytest_abra.dir_manager import DirManager 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() page = context.new_page()
url = "https://" + dotenv_config["DOMAIN"] url = "https://" + env_config["DOMAIN"]
page.goto(url) page.goto(url)
expect(page.locator(".wp-block-heading")).to_be_visible() 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!") expect(page.get_by_role("heading")).to_have_text("Willkommen bei WordPress!")

View file

@ -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

View file

@ -20,7 +20,7 @@ def test_complex_sorting() -> None:
] ]
demo_types = ["a", "b", "c", "d", "e", "f", "g"] 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 EnvManager.sort_env_files_by_rule
sorted_env_files = EnvManager.sort_env_files_by_rule(env_files, demo_rules) 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"] 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): with pytest.raises(ValueError):
EnvManager.sort_env_files_by_rule(env_files, demo_rules) EnvManager.sort_env_files_by_rule(env_files, demo_rules)