diff --git a/integration_tests/Makefile b/integration_tests/Makefile new file mode 100644 index 0000000..5c0b573 --- /dev/null +++ b/integration_tests/Makefile @@ -0,0 +1,15 @@ +PYTEST:=./venv/bin/pytest +PIP:=./venv/bin/pip + +.PHONY: clean +clean: + rm -rf ./venv + +.PHONY: venv +venv: clean + python3.11 -m venv venv + $(PIP) install -r requirements.txt + +.PHONY: test +test: + $(PYTEST) diff --git a/integration_tests/README.md b/integration_tests/README.md new file mode 100644 index 0000000..9c56fe3 --- /dev/null +++ b/integration_tests/README.md @@ -0,0 +1,28 @@ +# Elekto integration tests +This directory contains the integration tests for Elekto. These test focus on the integration with Github and the way +Github usernames flow through the Elekto application. + +## Running tests +The setup is not bootstrapped (yet), so various manual steps are required to run the required local infra. Then the +integration tests can be run. + +### Infra +These tests require the following to be running: +- elekto + - Change the Github endpoints in `elekto/constants.py` to: + - GITHUB_AUTHORIZE = 'http://localhost:9000/login/oauth/authorize' + - GITHUB_ACCESS = 'http://localhost:9000/login/oauth/access_token' + - GITHUB_PROFILE = 'http://localhost:9000/user' + - Start with `python console --run` (in the Elekto project). + - Optionally also do `docker compose up` if you want to use the Postgres database it provides. +- github-static-mock + - https://github.com/oduludo/github-oauth-mock + - Start the required Redis server with `docker compose up` in the github-oauth-mock project. + - Install dependencies with `poetry install`. + - Start the mock server with `poetry run start`. + +### Tests +Tests can be run from the `elekto/integration_tests` directory. Tests runner is Pytest, headless browser testing is done +using Playwright. A virtual environment is required to run the tests. Tests assume all infra runs at the default ports. +- Create the virtual env with `make venv`. This will also install dependencies. +- Run tests with `make test`. diff --git a/integration_tests/requirements.txt b/integration_tests/requirements.txt new file mode 100644 index 0000000..d5efb05 --- /dev/null +++ b/integration_tests/requirements.txt @@ -0,0 +1,28 @@ +attrs==25.4.0 +certifi==2025.10.5 +charset-normalizer==3.4.3 +greenlet==3.2.4 +h11==0.16.0 +idna==3.10 +iniconfig==2.1.0 +outcome==1.3.0.post0 +packaging==25.0 +playwright==1.55.0 +pluggy==1.6.0 +pyee==13.0.0 +Pygments==2.19.2 +PySocks==1.7.1 +pytest==8.4.2 +pytest-base-url==2.1.0 +pytest-playwright==0.7.1 +python-slugify==8.0.4 +requests==2.32.5 +sniffio==1.3.1 +sortedcontainers==2.4.0 +text-unidecode==1.3 +trio==0.31.0 +trio-websocket==0.12.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +websocket-client==1.9.0 +wsproto==1.2.0 diff --git a/integration_tests/tests/conftest.py b/integration_tests/tests/conftest.py new file mode 100644 index 0000000..5161efa --- /dev/null +++ b/integration_tests/tests/conftest.py @@ -0,0 +1,33 @@ +import pytest + +from utils.github_mock.client import GithubMockUtilityClient + + +@pytest.fixture +def host() -> str: + return 'http://localhost:8000' + + +@pytest.fixture +def login_url(host: str) -> str: + return f'{host}/login' + + +@pytest.fixture +def logout_url(host: str) -> str: + return f'{host}/logout' + + +@pytest.fixture +def app_url(host: str) -> str: + return f'{host}/app' + + +@pytest.fixture +def github_mock_host() -> str: + return 'http://localhost:9000' + + +@pytest.fixture +def github_mock_utility(github_mock_host: str) -> GithubMockUtilityClient: + return GithubMockUtilityClient(host=github_mock_host) diff --git a/integration_tests/tests/test_login.py b/integration_tests/tests/test_login.py new file mode 100644 index 0000000..a781f45 --- /dev/null +++ b/integration_tests/tests/test_login.py @@ -0,0 +1,38 @@ +import pytest +from playwright.sync_api import Page, expect + +from utils.github_mock import GithubMockUtilityClient, User + + +def logout(page: Page) -> None: + page.goto('http://localhost:8000/app') + logout_link = page.get_by_role('link', name='Logout') + if logout_link.is_visible(): + logout_link.click() + + +@pytest.mark.parametrize( + 'user', + [ + User(name='Jack', login='jack'), + User(name='Jill', login='jill'), + ] +) +def test_login(page: Page, login_url: str, logout_url: str, github_mock_utility: GithubMockUtilityClient, user: User) -> None: + """ + Test that login is working as expected. + + Multiple users should be able to log in and the dashboard should change content based on the authenticated user. + """ + logout(page) # Ensure we start with fresh state + + github_mock_utility.store_upcoming_user(user=user) + + page.goto(login_url) + + expect(page.get_by_text('Sign in with Github')).to_be_visible() + + page.get_by_role('button', name='Sign in with Github').click() + expect(page).to_have_title('Dashboard | Elekto') + expect(page.get_by_text(f'Welcome! {user.name}')).to_be_visible() + logout(page) diff --git a/integration_tests/tests/utils/__init__.py b/integration_tests/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/integration_tests/tests/utils/github_mock/__init__.py b/integration_tests/tests/utils/github_mock/__init__.py new file mode 100644 index 0000000..55ab167 --- /dev/null +++ b/integration_tests/tests/utils/github_mock/__init__.py @@ -0,0 +1,4 @@ +from .client import GithubMockUtilityClient +from .models import User + +__all__ = ['GithubMockUtilityClient', 'User'] diff --git a/integration_tests/tests/utils/github_mock/client.py b/integration_tests/tests/utils/github_mock/client.py new file mode 100644 index 0000000..93d9c96 --- /dev/null +++ b/integration_tests/tests/utils/github_mock/client.py @@ -0,0 +1,28 @@ +import requests + +from .models import User + + +class GithubMockUtilityClient: + def __init__(self, host: str): + self.host = host + + def store_upcoming_user(self, user: User) -> None: + """ + Set user data for the next mocked Github login. + + IRL users would log in at Github and then return to Elekto with a code. Elekto uses the code to get data for + that authenticated user. Our tests mock the Github part, causing the code to not point to a particular user. To + make up for this, we can set fake user data in the mock server. The next user lookup populates the 'name' and + 'login' fields with the fake data we sent in using the /system/upcoming-user call. + + Args: + user: The User object to set mock data from. + + Returns: None + + """ + resp = requests.post(f'{self.host}/system/upcoming-user', json=user.to_dict()) + + if resp.status_code != 201: + raise Exception(resp.text) diff --git a/integration_tests/tests/utils/github_mock/models.py b/integration_tests/tests/utils/github_mock/models.py new file mode 100644 index 0000000..d54c1ea --- /dev/null +++ b/integration_tests/tests/utils/github_mock/models.py @@ -0,0 +1,10 @@ +class User: + def __init__(self, name: str, login: str): + self.name = name + self.login = login + + def to_dict(self) -> dict: + return { + 'login': self.login, + 'name': self.name, + }