Skip to content

Commit

Permalink
Add encryption for sensitive data
Browse files Browse the repository at this point in the history
  • Loading branch information
dmy.berezovskyi committed Jan 10, 2025
1 parent 2e411ca commit 8baea99
Show file tree
Hide file tree
Showing 21 changed files with 114 additions and 49 deletions.
7 changes: 7 additions & 0 deletions .idea/dbnavigator.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions .idea/simple-python-selenium-framework.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@ A simple UI automation framework built with:
- Integrated with **GitHub Actions** CI workflow for Darwin (Mac) and Linux.
- Supports multiple environments: **dev**, **stage**.
- Generates **pytest reports** and **custom logs**.
- Secure Secrets Handling: Sensitive data like passwords are encrypted and stored securely.
- Test Data Management: Integrated with YAML files for test data storage and access.

## Getting Started

### Prerequisites

- Python 3.8 - 3.12
- If you're not using macOS with ARM64 architecture or a Selenium version below 4.24.0, please upload the appropriate driver corresponding to your OS to the `resources` directory.
- secure-test-automation for encrypting sensitive data

### Local Usage

Expand Down Expand Up @@ -68,7 +71,7 @@ A simple UI automation framework built with:
| 1. Drivers factory: local, remote, [Chrome, Firefox] | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 2. `pytest.ini` config: addopts, errors, markers | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 3. Environments: dev, stag, prod | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 4. Secrets | ![Status](https://img.shields.io/badge/TODO-yellow) |
| 4. Secrets | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 5. Utilities: YAML reader, logger | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 6. BasePage: wait strategy, base actions | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
| 7. Properties: make properties helper | ![Status](https://img.shields.io/badge/DONE-brightgreen) |
Expand Down Expand Up @@ -107,4 +110,20 @@ The linting configuration defines rules that dictate the checks performed. Custo
```plaintext
$FilePathRelativeToProjectRoot$ --config .ruff.toml
```
### Secrets
To secure our passwords or sensitive data, we store them in an encrypted form. For this, we use the [secure-test-automation](https://pypi.org/project/secure-test-automation/) library.
implementation on framework side: utils/crypto.py
The encryption key is stored in `/config/key.properties` (this file should be added to `.gitignore`) for local testing.
For executing tests in remote environments (e.g., BrowserStack, Jenkins, etc.), we have integrated the Vault HashiCorp library.

```python
@pytest.fixture
def get_password():
secure = Secure()
read = YAMLReader.read("data.yaml", to_simple_namespace=True)
password = read.users.john.details.password
return secure.decrypt_password(password)
```


### Demo tool https://demoqa.com/text-box
4 changes: 2 additions & 2 deletions config/data.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
users:
username1:
john:
username: "[email protected]"
details:
first_name: "John"
last_name: "Doe"
password: ""
password: gAAAAABngSXGxzoV1WZASXEj7VbEH37pOJ4_vC9gcdUnSpUI46fPOvTxhQ4-p_JNdCpRqmDlqX4nE0c9ISBB3-0-lWs2WcvdAQ==
username2:
username: "username"
names:
Expand Down
1 change: 1 addition & 0 deletions config/key.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rd-xPFY051UmVWPmmNoMXkq3qbuCJ7Ajc0BY7MtXm6g=
4 changes: 2 additions & 2 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from dotenv import load_dotenv
from selenium.webdriver.support.event_firing_webdriver import EventFiringWebDriver

from core.event_listener import EventListener
from core.driver_factory import WebDriverFactory
from core_driver.event_listener import EventListener
from core_driver.driver_factory import WebDriverFactory
from utils.logger import Logger, LogLevel

log = Logger(log_lvl=LogLevel.INFO).get_instance()
Expand Down
17 changes: 5 additions & 12 deletions core/driver.py → core_driver/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.remote.remote_connection import RemoteConnection
from webdriver_manager.chrome import ChromeDriverManager
from core.driver_options import _init_driver_options
from core_driver.driver_options import _init_driver_options
from utils.error_handler import ErrorHandler, ErrorType
from utils.logger import Logger, LogLevel
from properties import Properties
Expand Down Expand Up @@ -57,16 +57,13 @@ def create_driver(self, environment=None, dr_type="local"):
driver_path = ChromeDriverManager().install()
options = _init_driver_options(dr_type=dr_type)
driver = webdriver.Chrome(
service=ChromeService(executable_path=driver_path),
options=options
service=ChromeService(executable_path=driver_path), options=options
)
log.info(
f"Created local Chrome driver with session: {driver.session_id}"
)
except Exception as e:
log.error(
f"Failed to create Chrome driver {e}"
)
log.error(f"Failed to create Chrome driver {e}")
driver = webdriver.Chrome(
service=ChromeService(_get_driver_path(dr_type)),
options=_init_driver_options(dr_type=dr_type),
Expand All @@ -82,18 +79,14 @@ def create_driver(self, environment=None, dr_type=None):
command_executor=RemoteConnection("your remote URL"),
desired_capabilities={"LT:Options": caps}, # noqa
)
log.info(
f"Remote Chrome driver created with session: {driver.session_id}"
)
log.info(f"Remote Chrome driver created with session: {driver.session_id}")
return driver


class FirefoxDriver(Driver):
def create_driver(self, environment=None, dr_type=None):
try:
driver = webdriver.Firefox(
options=_init_driver_options(dr_type=dr_type)
)
driver = webdriver.Firefox(options=_init_driver_options(dr_type=dr_type))
log.info(f"Created Firefox driver with session: {driver.session_id}")
except Exception as e:
driver = webdriver.Chrome(
Expand Down
2 changes: 1 addition & 1 deletion core/driver_factory.py → core_driver/driver_factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from core.driver import ChromeRemoteDriver, FirefoxDriver, LocalDriver
from core_driver.driver import ChromeRemoteDriver, FirefoxDriver, LocalDriver
from utils.error_handler import ErrorHandler, ErrorType
from utils.logger import Logger, LogLevel

Expand Down
6 changes: 2 additions & 4 deletions core/driver_options.py → core_driver/driver_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
def _shared_driver_options(options):
options.add_argument("--start-maximized")
options.add_argument("--disable-dev-shm-usage")
options.page_load_strategy = 'none' # disable waiting for fully load page
options.page_load_strategy = "none" # disable waiting for fully load page
if platform.system() == "Linux":
options.add_argument("--no-sandbox")
log.info(f"Driver options {options.arguments}")
Expand All @@ -25,9 +25,7 @@ def _init_driver_options(dr_type=None):
options = driver_option_mapping.get(dr_type)

if options is None:
raise ErrorHandler.raise_error(
ErrorType.UNSUPPORTED_DRIVER_TYPE, dr_type
)
raise ErrorHandler.raise_error(ErrorType.UNSUPPORTED_DRIVER_TYPE, dr_type)

_shared_driver_options(options)
log.info(f"Driver options {options.arguments}")
Expand Down
4 changes: 1 addition & 3 deletions core/event_listener.py → core_driver/event_listener.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from selenium.webdriver.support.abstract_event_listener import (
AbstractEventListener
)
from selenium.webdriver.support.abstract_event_listener import AbstractEventListener


class EventListener(AbstractEventListener):
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ readme = "README.md"
packages = [
{ include = "utils" },
{ include = "scraper" },
{ include = "core" }
{ include = "core_driver" }
]
[tool.poetry.dependencies]
python = "^3.12"
Expand All @@ -19,11 +19,12 @@ python-dotenv = "1.0.1"
asyncio = "3.4.3"
aioselenium = "0.0.1"
pytest-xdist="3.3.1"
cryptography="43.0.1"
cryptography="44.0.0"
beautifulsoup4="4.12.2"
requests="^2.31.0"
setuptools="70.0.0"
ruff="0.6.8"
secure-test-automation="^1.3.1"


[tool.pytest.ini_options]
Expand Down
6 changes: 5 additions & 1 deletion src/locators/locators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

class General:
LOGO = (By.ID, "userForm")
ELEMENTS = (By.XPATH, "//h5[text()='Elements']")
FORMS = (By.XPATH, "//div[@class='card-body']//h5[text()='Forms']")
STORE = (By.XPATH, "//h5[text()='Book Store Application']")


class TextFields:
class TextBoxFields:
USER_NAME = (By.ID, "userName")
TEXT_BOX = (By.XPATH, "//span[text()='Text Box']")
4 changes: 4 additions & 0 deletions src/pageobjects/base_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,7 @@ def get_current_url(self):
def refresh(self):
"""Refresh the current page."""
self.driver.refresh()

def scroll_to_element(self, element):
"""Sroll to element"""
self.driver.execute_script("arguments[0].scrollIntoView();", element)
9 changes: 7 additions & 2 deletions src/pageobjects/text/fill_form.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from src.locators.locators import TextFields
from src.locators.locators import TextBoxFields, General
from src.pageobjects.base_page import BasePage
from utils.logger import log

Expand All @@ -7,4 +7,9 @@ class FillForm(BasePage):
@log()
def enter_username(self, name: str):
"""Enter username"""
self.set(TextFields.USER_NAME, name)
self.set(TextBoxFields.USER_NAME, name)

@log()
def enter_password(self, password):
"""Enter password"""
self.set(TextBoxFields.USER_NAME, password)
23 changes: 21 additions & 2 deletions tests/test_fill_form/test_fill_form.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
import time

import pytest
from src.pageobjects.text.fill_form import FillForm
from utils.crypto import Secure
from utils.yaml_reader import YAMLReader


class TestFillForm:
@pytest.fixture
def get_password():
secure = Secure()
read = YAMLReader.read("data.yaml", to_simple_namespace=True)
password = read.users.john.details.password
print(password)
return secure.decrypt_password(password)


def test_fill_user(self, make_driver):
class TestFillForm:
def test_fill_user(self, make_driver, get_password):
fill = FillForm(make_driver)

# Use the password provided by the fixture
password = get_password
print(password)

fill.enter_username("selenium framework")
fill.enter_password(password)
20 changes: 20 additions & 0 deletions utils/crypto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import pathlib
from typing import Optional, Literal

from core.cipher import Cipher


class Secure:
def __init__(
self,
base_path: Optional[pathlib.Path] = None,
store_type: Literal["local", "vault"] = "local",
):
self.base_path = (
base_path or pathlib.Path(__file__).resolve().parent.parent / "config"
)
self.store_type = store_type
self.cipher = Cipher(base_path=self.base_path, vault_type=store_type)

def decrypt_password(self, password: bytes):
return self.cipher.decrypt(password)
10 changes: 0 additions & 10 deletions utils/decrypt.py

This file was deleted.

2 changes: 1 addition & 1 deletion utils/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ErrorHandler:
ErrorType.ENV_ERROR: "Unsupported environment",
ErrorType.EMPTY_URL_ERROR: "Environment variable is empty or not found",
ErrorType.UNSUPPORTED_DRIVER_TYPE: "Unsupported driver type",
ErrorType.DRIVER_NOT_FOUND: "WebDriver binary not found at "
ErrorType.DRIVER_NOT_FOUND: "WebDriver binary not found at ",
}

@staticmethod
Expand Down
1 change: 1 addition & 0 deletions utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def wrapper(*args, **kwargs):

def timing(func):
"""Perform a timing function"""

@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
Expand Down
4 changes: 1 addition & 3 deletions utils/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,7 @@ def wrapper(self, *args, **kwargs) -> Any:
logger_instance.annotate(logs, level)

# Call the original method, passing *args and **kwargs
return func(
self, *args, **kwargs
)
return func(self, *args, **kwargs)

return wrapper

Expand Down

0 comments on commit 8baea99

Please sign in to comment.