Skip to content

Commit

Permalink
feature/TSS-2293-trs-dbt-migration-create-end-to-end-test-scripts (#430)
Browse files Browse the repository at this point in the history
* wip

* adding end to end test scripts

* adding more tests

* adding read me for end to end testing

* adding better heading and styling

* added completed end to end public registeration

* adding login test
  • Loading branch information
osimuka authored Dec 17, 2024
1 parent 670b280 commit 6d789fa
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ all-requirements:
poetry export --without-hashes -f requirements.txt -o requirements.txt
poetry export --with dev --without-hashes -f requirements.txt -o requirements-dev.txt
pip install -r requirements-dev.txt

.PHONY: test-end-to-end
is-headless ?= false
test-end-to-end:
./run_e2e_tests.sh target_url=$(target_url) target=$(target) $(if $(filter true,$(is-headless)),--is-headless)
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,44 @@ Behavioural testing is provided by [Behave Django](https://github.com/behave/beh

from the Trade Remedies orchestration project directory.


## Running End to End tests using playwright with pytest
Playwright documentation - https://playwright.dev/python/docs/api/class-playwright

The end-to-end frontend tests reside in the e2e directory and are designed to operate independently of the rest of the application. This autonomy is facilitated through a local pytest.ini configuration file located within the same directory. The pytest.ini file configures specific parameters and settings essential for the execution of these tests, ensuring they can run in a self-contained environment. For detailed customization options and further information on pytest configuration files, refer to the [pytest configuration docs](https://docs.pytest.org/en/7.0.x/reference/customize.html)

If you are running the docker build

1. Ensure the API is running & the frontend service is runing and can be accessed on `http://localhost:{frontend_port}` if runing within the docker container

2. Ensure the frontend server is up and has reached the point where the Django development server is running.

By default the tests DO NOT RUN in headless mode, to activate headless mode the variable --is-headless will be required.

3. Run the tests:
`make test-end-to-end target_url=<target-url>` e.g target_url: `http://localhost:8002/` or `https://trade-remedies-public-uat.london.cloudapps.digital/`

4. To run a specific suite of frontend tests, specify the desired module:
`make test-end-to-end target_url=<target-url> target=test_examples.py`

To run headless:
`make test-end-to-end target_url=http://localhost:8002/ is-headless=true`

#### setup pytest & playwright end to end module

___module structure___

```
e2e/
├── .gitignore # Specifies intentionally untracked files to ignore
├── requirements.txt # Project dependencies
├── conftest.py # the pytest config file (the most important file to get things going)
├── README.md # The top-level README for developers using this project
└── pytest.ini # Configuration file for pytest
└── test_file.py # one test file for a specific end to end functionality
...
```

## Fitness Functions
![Current fitness metrics for TRSV2](fitness/fitness_metrics_graph.png)

Expand Down
Empty file added e2e/__init__.py
Empty file.
63 changes: 63 additions & 0 deletions e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import os
import pytest
from playwright.sync_api import sync_playwright

BASE_URL = os.getenv("BASE_FRONTEND_TESTING_URL", "http://localhost:8002/")
HEADLESS = os.getenv("TEST_HEADLESS", "false").lower() == "true"


@pytest.fixture(scope="session")
def session_data():
"""Return a dictionary to store session data."""
return {
"cookies": None,
"email": None,
"password": None,
}


@pytest.fixture(scope="session")
def playwright_instance():
"""Return a Playwright instance."""
with sync_playwright() as p:
yield p


@pytest.fixture(scope="session")
def browser(playwright_instance):
"""Return a browser instance."""
if HEADLESS:
browser = playwright_instance.chromium.launch(headless=True)
else:
browser = playwright_instance.chromium.launch(slow_mo=100, headless=HEADLESS)
yield browser
browser.close()


@pytest.fixture(scope="session")
def context(browser, session_data):
# Create a new browser context
context = browser.new_context()
context.set_default_timeout(0)

# Initially, session_data["cookies"] will be None.
# Check if "cookies" key exists and has a value; if not, it means it's the first test run.
if session_data.get("cookies") is None:
# Since it's the first run, let the browser context initiate and capture the cookies.
session_data["cookies"] = context.cookies()
else:
# If it's not the first run, load the initially captured cookies into the context.
context.add_cookies(session_data["cookies"])

yield context
context.close()


@pytest.fixture(scope="session")
def page(context):
# Create a new page in the provided context
_page = context.new_page()
_page.goto(BASE_URL, wait_until="domcontentloaded")
# Wait for the page to load
_page.wait_for_timeout(10000)
yield _page
4 changes: 4 additions & 0 deletions e2e/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
testpaths = e2e
python_files = test_*.py
python_functions = test_*
10 changes: 10 additions & 0 deletions e2e/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
faker==19.3.1 ; python_version >= "3.9" and python_version < "4.0"
pytest-cov==2.10.1 ; python_version >= "3.8" and python_version < "4.0"
pytest-django==4.5.2 ; python_version >= "3.8" and python_version < "4.0"
pytest-forked==1.6.0 ; python_version >= "3.8" and python_version < "4.0"
pytest-order==1.1.0 ; python_version >= "3.8" and python_version < "4.0"
pytest-watch==4.2.0 ; python_version >= "3.8" and python_version < "4.0"
pytest-xdist==2.1.0 ; python_version >= "3.8" and python_version < "4.0"
pytest==8.0.1 ; python_version >= "3.8" and python_version < "4.0"
pytest-dependency==0.6.0 ; python_version >= "3.8" and python_version < "4.0"
playwright==1.41.2 ; python_version >= "3.8" and python_version < "4.0"
110 changes: 110 additions & 0 deletions e2e/test_public_register_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import pytest

from playwright.sync_api import expect

from e2e.utils import get_base_url, retry, generate_test_name, generate_test_email, generate_test_password, generate_test_address, genetrate_test_postcode

BASE_URL = get_base_url()

@retry()
def test_public_login(page):
page.goto(BASE_URL)
expect(page.get_by_role("heading", name="Trade Remedies Service: sign")).to_be_visible()
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_role("button", name="Sign in")).to_be_visible()


@retry()
def test_public_register(page):
page.goto(BASE_URL)
expect(page.get_by_role("heading", name="Trade Remedies Service: sign")).to_be_visible()
expect(page.get_by_role("link", name="Create an account")).to_be_visible()
page.get_by_role("link", name="Create an account").click()
page.get_by_role("heading", name="Has anyone else from your").click()
page.get_by_label("No").check()
page.get_by_role("button", name="Continue").click()
expect(page.get_by_role("heading", name="Create an account")).to_be_visible()


@retry()
@pytest.mark.dependency(name="test_register_user_with_new_org", scope="session")
def test_register_user_with_new_org(page, session_data):

email = generate_test_email()
name = generate_test_name()
password = generate_test_password()
address = generate_test_address()
postcode = genetrate_test_postcode()

page.goto(BASE_URL)
expect(page.get_by_role("heading", name="Trade Remedies Service: sign")).to_be_visible()
expect(page.get_by_role("link", name="Create an account")).to_be_visible()
page.get_by_role("link", name="Create an account").click()
page.get_by_role("heading", name="Has anyone else from your").click()
page.get_by_label("No").check()
page.get_by_role("button", name="Continue").click()
page.get_by_label("Enter your name").click()
page.get_by_label("Enter your name").fill(name)
page.get_by_label("Your email address").click()
page.get_by_label("Your email address").fill(email)
page.get_by_label("I have read and understood").check()
page.get_by_role("button", name="Continue").click()
page.get_by_label("Enter Password").click()
page.get_by_label("Enter Password").fill(password)
page.get_by_role("button", name="Continue").click()
expect(page.get_by_role("heading", name="Two-factor authentication")).to_be_visible()
expect(page.get_by_text("How would you like to receive")).to_be_visible()
page.locator('input.govuk-radios__input[id="email"]').click()
page.get_by_role("button", name="Continue").click()
expect(page.get_by_text("Is your organisation a UK")).to_be_visible()
page.get_by_label("No").check()
page.get_by_role("button", name="Continue").click()
page.get_by_label("Name of your organisation").click()
page.get_by_label("Name of your organisation").fill(f"{name} LTD")
page.get_by_label("Street address").click()
page.get_by_label("Street address").fill(address)
page.get_by_label("Postcode or zip code").click()
page.get_by_label("Postcode or zip code").fill(postcode)
page.get_by_label("Country").select_option("GB")
page.get_by_label("Organisation registration").click()
page.get_by_label("Organisation registration").fill("12345678")
page.get_by_role("button", name="Continue").click()
expect(page.get_by_role("heading", name="Further details about your")).to_be_visible()
expect(page.get_by_role("button", name="Create my account")).to_be_visible()
page.get_by_role("button", name="Create my account").click()
page.locator("#main-content").get_by_role("link", name="Sign in").click()
expect(page.get_by_role("heading", name="Sign in to Trade Remedies")).to_be_visible()

# store user name and password for later use in another test
session_data["email"] = email
session_data["password"] = password


@retry()
def test_login_with_invalid_credentials(page):
page.goto(BASE_URL)
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_role("heading", name="Sign in to Trade Remedies")).to_be_visible()
page.get_by_label("Email address").click()
page.get_by_label("Email address").fill("[email protected]")
page.get_by_label("Password").click()
page.get_by_label("Password").fill("123456789")
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_label("There is a problem")).to_be_visible()


@retry()
@pytest.mark.dependency(depends=["test_register_user_with_new_org"], scope="session")
def test_login_with_valid_credentials(page, session_data):
email = session_data["email"]
password = session_data["password"]
page.goto(BASE_URL)
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_role("heading", name="Sign in to Trade Remedies")).to_be_visible()

page.get_by_label("Email address").click()
page.get_by_label("Email address").fill(email)
page.get_by_label("Password").click()
page.get_by_label("Password").fill(password)
page.get_by_role("button", name="Sign in").click()
expect(page.get_by_text("Verify your email address", exact=True)).to_be_visible()
116 changes: 116 additions & 0 deletions e2e/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import os
import random
import string
import inspect
import re
import time
import pytest

from functools import wraps

def get_base_url():
return os.getenv("BASE_FRONTEND_TESTING_URL", "http://localhost:8002/")

def clean_full_url(url):
"""Clean a URL by removing multiple slashes and trailing slash."""

# Split the URL into protocol and the rest.
if "://" in url:
protocol, rest = url.split("://", 1)
# Clean the 'rest' part of the URL by replacing multiple slashes with a single one.
rest = re.sub(r"/+", "/", rest)
# Remove trailing slash if present
if rest.endswith('/'):
rest = rest[:-1]
# Concatenate the protocol and the cleaned part back together.
cleaned_url = f"{protocol}://{rest}"
else:
# If there's no protocol, just clean the URL directly.
cleaned_url = re.sub(r"/+", "/", url)
# Remove trailing slash if present
if cleaned_url.endswith('/'):
cleaned_url = cleaned_url[:-1]
return cleaned_url


def retry(tries=3, delay=3, backoff=2, logger=None):
"""Retry calling the decorated function using an exponential backoff."""

def deco_retry(f):
@wraps(f)
def f_retry(*args, **kwargs):
mtries, mdelay = tries, delay
# Get argument names of the function
arg_names = inspect.getfullargspec(f).args

# Run the function with retries
while mtries > 1:
try:
return f(*args, **kwargs)
except TimeoutError as e:
msg = f"{e}, Retrying in {mdelay} seconds..."
if logger:
logger.warning(msg)
else:
print(msg)
time.sleep(mdelay)
mtries -= 1
mdelay *= backoff

# Final attempt
return f(*args, **kwargs)

# If the function expects a fixture named 'request', assume it's a pytest test
if "request" in inspect.getfullargspec(f).args:
# Use pytest's request fixture to modify the function with retry logic
@pytest.fixture
def wrapper(request, *args, **kwargs):
# Modify the test function by injecting the retry decorator
return f_retry(*args, **kwargs)

return wrapper
else:
return f_retry

return deco_retry


def generate_test_name():
"""Generate a random sample user name."""
prefix = "user_"
suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
return prefix + suffix


def generate_test_email():
"""Generate a random sample email address."""
prefix = "test_"
suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
name = ".".join([prefix, suffix])
domain = "@gov.uk"
return name + domain


def generate_test_password():
"""Generate a random sample password with at least 8 characters and at most 12 characters."""
# password needs to be in the format
# capital letter, lowercase letter, number, special character
password = ''.join(random.choices(string.ascii_uppercase, k=1))
password += ''.join(random.choices(string.ascii_lowercase, k=1))
password += ''.join(random.choices(string.digits, k=1))
password += ''.join(random.choices(string.punctuation, k=1))

# We now add 8 more characters to reach a total of 12
password += ''.join(random.choices(string.ascii_letters + string.digits + string.punctuation, k=8))

return password


def generate_test_address():
"""Generate a random address."""
return ''.join(random.choices(string.ascii_letters + string.digits, k=8))


def genetrate_test_postcode():
"""Generate a random postcode."""
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=7))
Loading

0 comments on commit 6d789fa

Please sign in to comment.