-
Notifications
You must be signed in to change notification settings - Fork 0
AP-638: Implements e2e tests using Playwright #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| # The Playwright server (here) and client (from requirements.txt) | ||
| # must match down to the minor version. If they differ, expect to | ||
| # see a clear error when running the e2e tests. | ||
| ARG PLAYWRIGHT_VERSION=1.58.2 | ||
|
|
||
| FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy | ||
|
|
||
| # socat runs per-port TCP forwarders that translate localhost:8080 and | ||
| # localhost:8180 inside this container to airflow-apiserver:8080 and | ||
| # keycloak:8180 on the compose network. This lets Chromium drive the | ||
| # Airflow UI via the exact same localhost URLs a host-browser developer | ||
| # uses, so webserver_config.py and Keycloak's KC_HOSTNAME stay prod-parity. | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends socat \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
|
|
||
| COPY entrypoint.sh /usr/local/bin/playwright-sidecar-entrypoint.sh | ||
| RUN chmod +x /usr/local/bin/playwright-sidecar-entrypoint.sh | ||
|
|
||
| # Inherits from the top-level ARG | ||
| ARG PLAYWRIGHT_VERSION | ||
| ENV PLAYWRIGHT_VERSION=$PLAYWRIGHT_VERSION | ||
| RUN npm install -g playwright@${PLAYWRIGHT_VERSION} | ||
|
|
||
| ENTRYPOINT ["/usr/local/bin/playwright-sidecar-entrypoint.sh"] | ||
| CMD ["playwright", "run-server", "--port", "3000", "--host", "0.0.0.0", "--path", "/ws"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #!/bin/bash | ||
| set -euo pipefail | ||
|
|
||
| # Port forwarders: make localhost:<port> inside this container transparently | ||
| # reach the named compose services. See this container's Dockerfile for why. | ||
| socat TCP-LISTEN:8080,fork,reuseaddr TCP:airflow-apiserver:8080 & | ||
| socat TCP-LISTEN:8180,fork,reuseaddr TCP:keycloak:8180 & | ||
|
|
||
| exec "$@" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| """ | ||
| conftest.py | ||
|
|
||
| Fixtures and helpers for e2e tests. | ||
| """ | ||
|
|
||
| import os | ||
| import re | ||
|
|
||
| from collections.abc import Generator | ||
|
|
||
| import pytest | ||
|
|
||
| from playwright.sync_api import Browser, BrowserContext, Page, Playwright | ||
|
|
||
|
|
||
| # Defaults match a host-browser developer's URLs. Chromium inside the | ||
| # Playwright sidecar reaches these via localhost TCP forwarders installed | ||
| # by playwright/entrypoint.sh, so the OAuth flow hits the same code paths | ||
| # as a human driving the UI from their workstation. Host devs running | ||
| # pytest from their shell only need to override PYTEST_PLAYWRIGHT_WS_ENDPOINT. | ||
| AIRFLOW_BASE_URL = os.environ.get("PYTEST_AIRFLOW_BASE_URL", "http://localhost:8080") | ||
| KEYCLOAK_BASE_URL = os.environ.get("PYTEST_KEYCLOAK_BASE_URL", "http://localhost:8180") | ||
|
danschmidt5189 marked this conversation as resolved.
|
||
| PLAYWRIGHT_WS_ENDPOINT = os.environ.get("PYTEST_PLAYWRIGHT_WS_ENDPOINT", "ws://playwright:3000/ws") | ||
|
|
||
|
|
||
| def airflow_url(path: str = ".*") -> re.Pattern[str]: | ||
| """ | ||
| Generates AirFlow URL matchers for tests | ||
|
|
||
| Use this when you want to assert that you're on AirFlow. Use `path` | ||
| to narrow down the expected path. | ||
|
|
||
| Ex. | ||
| page.wait_for_url(airflow_url('/security/users.*')) | ||
| """ | ||
| return re.compile(f"^{re.escape(AIRFLOW_BASE_URL)}{path}") | ||
|
|
||
|
|
||
| def airflow_login_url() -> re.Pattern[str]: | ||
| """ | ||
| Shorthand for matching AirFlow's login page | ||
| """ | ||
| return airflow_url("/auth/login.*") | ||
|
|
||
|
|
||
| def keycloak_url(path: str = ".*") -> re.Pattern[str]: | ||
| """ | ||
| Generates KeyCloak URL matchers for tests | ||
|
|
||
| Use this when you want to assert that you're on KeyCloak. | ||
| """ | ||
| return re.compile(f"^{re.escape(KEYCLOAK_BASE_URL)}{path}") | ||
|
|
||
|
|
||
| def keycloak_login_url() -> re.Pattern[str]: | ||
| """ | ||
| Shorthand for matching KeyCloak's mocked CalNet login page | ||
| """ | ||
| return keycloak_url("/realms/berkeley-local/protocol/openid-connect/auth.*") | ||
|
|
||
|
|
||
| def login(page: Page, username: str, password: str) -> None: | ||
| """Walk a fresh page through the Airflow -> Keycloak OIDC login flow. | ||
|
awilfox marked this conversation as resolved.
|
||
|
|
||
| Leaves the page on whatever Airflow returns after the post-login redirect — | ||
| the caller is responsible for asserting the authorized/denied outcome. | ||
| """ | ||
| page.goto("/") | ||
| page.wait_for_url(airflow_login_url()) | ||
| page.locator("#btn-signin-keycloak").click() | ||
| page.wait_for_url(keycloak_login_url()) | ||
| page.locator("#username").fill(username) | ||
| page.locator("#password").fill(password) | ||
| page.locator("#kc-login").click() | ||
| page.wait_for_url(airflow_url()) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def context(context) -> Generator[BrowserContext, None, None]: | ||
| """ | ||
| Shorten pytest-playwright's default timeouts to 5s | ||
| """ | ||
| context.set_default_timeout(5000) | ||
| context.set_default_navigation_timeout(5000) | ||
| yield context | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def page_as_testpublic(page: Page) -> Page: | ||
| login(page, 'testpublic', 'testpublic') | ||
| return page | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def page_as_testuser(page: Page) -> Page: | ||
| login(page, 'testuser', 'testuser') | ||
| return page | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def page_as_testadmin(page: Page) -> Page: | ||
| login(page, 'testadmin', 'testadmin') | ||
| return page | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def browser(playwright: Playwright) -> Generator[Browser, None, None]: | ||
| browser = playwright.chromium.connect(PLAYWRIGHT_WS_ENDPOINT) | ||
| yield browser | ||
| browser.close() | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def browser_context_args(browser_context_args: dict) -> dict: | ||
| return {**browser_context_args, "base_url": AIRFLOW_BASE_URL} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| """ | ||
| test_auth.py | ||
|
|
||
| Playwright-driven e2e tests for the OIDC login flow. | ||
| """ | ||
|
|
||
| import pytest | ||
|
danschmidt5189 marked this conversation as resolved.
|
||
|
|
||
| from playwright.sync_api import Page, expect | ||
|
danschmidt5189 marked this conversation as resolved.
|
||
|
|
||
| from .conftest import airflow_login_url, airflow_url | ||
|
|
||
| pytestmark = pytest.mark.e2e | ||
|
|
||
|
|
||
| # Matches the main div of the homepage. Absent from the login page. | ||
| HOMEPAGE_MARKER = '[data-testid="main-content"]' | ||
|
|
||
| # Matches the Admin tab in the left navbar. | ||
| ADMIN_MARKER = 'button[aria-label="Admin"]' | ||
|
|
||
|
|
||
| def test_airflow_requires_login(page: Page) -> None: | ||
| page.goto("/") | ||
| expect(page).to_have_url(airflow_login_url()) | ||
| expect(page.locator("#btn-signin-keycloak")).to_be_visible() | ||
|
|
||
|
|
||
| def test_public_user_cannot_view_homepage(page_as_testpublic: Page) -> None: | ||
| page_as_testpublic.goto('/') | ||
| expect(page_as_testpublic).to_have_url(airflow_login_url()) | ||
| expect(page_as_testpublic.locator(HOMEPAGE_MARKER)).not_to_be_visible() | ||
|
|
||
|
|
||
| def test_regular_user_can_view_homepage(page_as_testuser: Page) -> None: | ||
| page_as_testuser.goto('/') | ||
| expect(page_as_testuser).to_have_url(airflow_url("/")) | ||
| expect(page_as_testuser.locator(HOMEPAGE_MARKER)).to_be_visible() | ||
|
|
||
|
|
||
| def test_regular_user_cannot_view_admin_tab(page_as_testuser: Page) -> None: | ||
| page_as_testuser.goto('/') | ||
| expect(page_as_testuser).to_have_url(airflow_url("/")) | ||
| expect(page_as_testuser.locator(ADMIN_MARKER)).not_to_be_visible() | ||
|
|
||
|
|
||
| def test_admin_user_can_view_admin_tab(page_as_testadmin: Page) -> None: | ||
| page_as_testadmin.goto('/') | ||
| expect(page_as_testadmin).to_have_url(airflow_url("/")) | ||
| expect(page_as_testadmin.locator(ADMIN_MARKER)).to_be_visible() | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.