diff --git a/.gitignore b/.gitignore index 088663472..dec639ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,10 +24,14 @@ vendor/ scratch config.yml !playground/config.yml +*.pyc # Project exclusion site venv .cache # Binaries -cmd/greenmask/greenmask \ No newline at end of file +cmd/greenmask/greenmask + +# Allure artifacts +allure-results/ \ No newline at end of file diff --git a/.run/v1-list-dumps.run.xml b/.run/v1-list-dumps.run.xml new file mode 100644 index 000000000..7d99c0dee --- /dev/null +++ b/.run/v1-list-dumps.run.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/v1/playground/mysql/run.sh b/v1/playground/mysql/run.sh new file mode 100755 index 000000000..2568c1b15 --- /dev/null +++ b/v1/playground/mysql/run.sh @@ -0,0 +1,6 @@ +#!/bin/bash +echo "alias mysql='mysql --user $MYSQL_USER --database $ORIGINAL_DB_NAME --host $ORIGINAL_DB_HOST --port $ORIGINAL_DB_PORT'" >> ~/.bashrc +echo "alias mysql_o='mysql --user $MYSQL_USER --database $ORIGINAL_DB_NAME --host $ORIGINAL_DB_HOST --port $ORIGINAL_DB_PORT'" >> ~/.bashrc +echo "alias mysql_t='mysql --user $MYSQL_USER --database $TRANSFORMED_DB_NAME --host $TRANSFORMED_DB_HOST --port $TRANSFORMED_DB_PORT'" >> ~/.bashrc +echo "alias cleanup='/var/lib/playground/cleanup.sh'" >> ~/.bashrc +bash diff --git a/v1/tests/integration/README.md b/v1/tests/integration/README.md new file mode 100644 index 000000000..704e0a84b --- /dev/null +++ b/v1/tests/integration/README.md @@ -0,0 +1,41 @@ +# Integration Tests + +This directory contains Python-based integration tests for Greenmask using `pytest` and `allure`. + +## Prerequisites + +- Python 3.11+ +- Poetry (for dependency management) +- Docker & Docker Compose (for running Greenmask services) + +## Setup + +1. Install dependencies: + ```bash + poetry install + ``` + +2. (Optional) Run Greenmask environment: + Ensure your `greenmask` service or target environment is running. + Export the service URL if different from default: + ```bash + export GREENMASK_URL=http://localhost:8080 + ``` + +## Running Tests + +Run tests with Allure (report data generated in `allure-results`): +```bash +poetry run pytest --alluredir=./allure-results +``` + +To view the Allure report: +```bash +poetry run allure serve ./allure-results +``` + +## Structure + +- `tests/`: Contains test files. +- `tests/conftest.py`: Fixtures (async client, configuration). +- `pyproject.toml`: Dependency and tool configuration. diff --git a/v1/tests/integration/integration.iml b/v1/tests/integration/integration.iml new file mode 100644 index 000000000..b2a201522 --- /dev/null +++ b/v1/tests/integration/integration.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/v1/tests/integration/poetry.lock b/v1/tests/integration/poetry.lock new file mode 100644 index 000000000..20d228f99 --- /dev/null +++ b/v1/tests/integration/poetry.lock @@ -0,0 +1,159 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "allure-pytest" +version = "2.15.2" +description = "Allure pytest integration" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "allure_pytest-2.15.2-py3-none-any.whl", hash = "sha256:9b472baa93b2860cad0eb03fced4091e6a75c6d822a1ec5b92fec5a403c84e10"}, + {file = "allure_pytest-2.15.2.tar.gz", hash = "sha256:6330b09df4dfec04296f1512ddb0be92e1a4741c36ba84e0388b96d327ac5f55"}, +] + +[package.dependencies] +allure-python-commons = "2.15.2" +pytest = ">=4.5.0" + +[[package]] +name = "allure-python-commons" +version = "2.15.2" +description = "Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks" +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "allure_python_commons-2.15.2-py3-none-any.whl", hash = "sha256:d5dd1b56d978d1a1e3f8dd4efd15037add1c457cfb6d0445695dffde6f196446"}, + {file = "allure_python_commons-2.15.2.tar.gz", hash = "sha256:19ef4a48bc6d30db484ce95dba74b639fcf87084746aac39d265ab3a1cc70e89"}, +] + +[package.dependencies] +attrs = ">=16.0.0" +pluggy = ">=0.4.0" + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.8" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[metadata] +lock-version = "2.1" +python-versions = "^3.11" +content-hash = "8814ecbe641256224197d234e79f071f259a8c1eb7e5fb7fc90d9ac8b9e1e541" diff --git a/v1/tests/integration/pyproject.toml b/v1/tests/integration/pyproject.toml new file mode 100644 index 000000000..9effb6f00 --- /dev/null +++ b/v1/tests/integration/pyproject.toml @@ -0,0 +1,20 @@ +[tool.poetry] +name = "integration" +version = "0.1.0" +description = "Integration tests for Greenmask" +authors = ["Greenmask Team"] +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.11" +pytest = "^8.0.0" +pytest-asyncio = "^0.23.0" +allure-pytest = "^2.13.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/v1/tests/integration/resources/__init__.py b/v1/tests/integration/resources/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/v1/tests/integration/resources/config.yaml b/v1/tests/integration/resources/config.yaml new file mode 100644 index 000000000..608208b4e --- /dev/null +++ b/v1/tests/integration/resources/config.yaml @@ -0,0 +1,37 @@ +# Copyright 2025 Greenmask +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +log: + level: "debug" + format: "text" + +storage: + type: "directory" + directory: + path: "/tmp/greenmask_backups" + +dump: + transformation: + - schema: "public" + name: "users" + transformers: + - name: "RandomPerson" + params: + gender: "Any" + columns: + - name: "username" + template: "{{ .FirstName }}" + engine: "random" + +# engine: postgresql # Commenting out to see if it fixes root key error diff --git a/v1/tests/integration/tests/conftest.py b/v1/tests/integration/tests/conftest.py new file mode 100644 index 000000000..f51e5aa02 --- /dev/null +++ b/v1/tests/integration/tests/conftest.py @@ -0,0 +1,75 @@ +import pytest +import os +import subprocess +import shutil +import logging +import allure + +logger = logging.getLogger(__name__) + +@pytest.fixture(scope="session") +def greenmask_bin(): + """Locates the greenmask binary. + Prioritizes GREENMASK_BIN env var. + """ + bin_path = os.getenv("GREENMASK_BIN") + if bin_path: + if not os.path.isfile(bin_path): + pytest.fail(f"GREENMASK_BIN is set to {bin_path} but it does not exist.") + return bin_path + + # Assumes greenmask is in PATH if not specified + bin_path = shutil.which("greenmask") + if not bin_path: + pytest.skip("greenmask binary not found in PATH and GREENMASK_BIN is not set.") + return bin_path + +@pytest.fixture +def greenmask_config(request, tmp_path): + """Sets up the configuration environment. + Copies the default config to a temp dir to allow tests to modify it if needed, + or just returns a path to a static config. + """ + # For now, let's just return the path to the resources config + # In the future, we might want to copy it to tmp_path + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + config_path = os.path.join(base_dir, "resources", "config.yaml") + + if not os.path.exists(config_path): + pytest.logger.warning(f"Config file not found at {config_path}") + + return config_path + +@pytest.fixture +def greenmask_cmd(greenmask_bin, greenmask_config): + """Executes greenmask commands. + """ + @allure.step("Run greenmask {args}") + def _run_greenmask(args, env=None, check=True): + cmd = [greenmask_bin, "--config", greenmask_config] + args + + full_env = os.environ.copy() + if env: + full_env.update(env) + + logger.info(f"Running command: {' '.join(cmd)}") + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=full_env, + ) + + if check and result.returncode != 0: + logger.error(f"Command failed with return code {result.returncode}") + logger.error(f"STDOUT: {result.stdout}") + logger.error(f"STDERR: {result.stderr}") + allure.attach(result.stdout, name="stdout", attachment_type=allure.attachment_type.TEXT) + allure.attach(result.stderr, name="stderr", attachment_type=allure.attachment_type.TEXT) + pytest.fail(f"greenmask command failed: {result.stderr}") + + allure.attach(result.stdout, name="stdout", attachment_type=allure.attachment_type.TEXT) + return result + + return _run_greenmask diff --git a/v1/tests/integration/tests/test_greenmask.py b/v1/tests/integration/tests/test_greenmask.py new file mode 100644 index 000000000..c35a19604 --- /dev/null +++ b/v1/tests/integration/tests/test_greenmask.py @@ -0,0 +1,17 @@ +@allure.feature("Allure Integration") +@allure.story("Report Generation") +def test_allure_dummy(): + """ + Dummy test to verify Allure report generation. + """ + with allure.step("Step 1"): + pass + with allure.step("Step 2"): + assert True + """ + Dummy test to verify Allure report generation. + """ + with allure.step("Step 1"): + pass + with allure.step("Step 2"): + assert True diff --git a/v1/tests/integration/tests/test_skeleton.py b/v1/tests/integration/tests/test_skeleton.py new file mode 100644 index 000000000..1fd649ad2 --- /dev/null +++ b/v1/tests/integration/tests/test_skeleton.py @@ -0,0 +1,22 @@ +import pytest +import allure + +@allure.feature("CLI") +@allure.story("Help Command") +@allure.title("Verify greenmask help command") +def test_greenmask_help(greenmask_cmd): + """Verifies that greenmask help command runs successfully. + """ + result = greenmask_cmd(["--help"]) + assert "Usage:" in result.stdout + assert result.returncode == 0 + +@allure.feature("CLI") +@allure.story("Transformers") +@allure.title("Verify greenmask list-transformers command") +def test_greenmask_list_transformers(greenmask_cmd): + """Verifies that greenmask list-transformers command runs successfully. + """ + result = greenmask_cmd(["list-transformers"]) + assert result.returncode == 0 + assert "RandomPerson" in result.stdout