Skip to content

Commit

Permalink
Merge pull request #110 from lsst-sqre/tickets/DM-26751
Browse files Browse the repository at this point in the history
[DM-26751] Add Selenium tests
  • Loading branch information
rra authored Sep 30, 2020
2 parents 04734f4 + 092ffde commit c76642e
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 37 deletions.
10 changes: 2 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,14 @@ repos:
name: Check rST
files: (README\.rst)|(CHANGELOG\.rst)

- repo: https://github.com/asottile/seed-isort-config
rev: v2.2.0
hooks:
- id: seed-isort-config
args: [--exclude=docs/.*\.py, --application-directories=src]

- repo: https://github.com/timothycrosley/isort
- repo: https://github.com/pycqa/isort
rev: 5.5.3
hooks:
- id: isort
additional_dependencies:
- toml

- repo: https://github.com/ambv/black
- repo: https://github.com/psf/black
rev: 20.8b1
hooks:
- id: black
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ exclude_lines = [

[tool.black]
line-length = 79
target-version = ["py37"]
target-version = ["py38"]
exclude = '''
/(
\.eggs
Expand All @@ -117,5 +117,4 @@ exclude = '''
include_trailing_comma = true
multi_line_output = 3
known_first_party = ["gafaelfawr", "tests"]
known_third_party = ["aiohttp", "aiohttp_csrf", "aiohttp_jinja2", "aiohttp_remotes", "aiohttp_session", "aioredis", "aioresponses", "cachetools", "click", "cryptography", "jinja2", "jwt", "mockaioredis", "pydantic", "pytest", "safir", "setuptools", "structlog", "wtforms", "yaml"]
skip = ["docs/conf.py"]
2 changes: 2 additions & 0 deletions requirements/dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pre-commit
pytest
pytest-aiohttp
pytest-sugar
selenium
selenium-wire
seqdiag
sphinx-automodapi==0.12
sphinx-click
Expand Down
10 changes: 9 additions & 1 deletion requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ requests==2.24.0 \
--hash=sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b \
--hash=sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898 \
# via documenteer, sphinx
selenium-wire==2.1.1 \
--hash=sha256:1121d7c9ab8539fbe9ed07f650d4bf0a104c108e3ea0b73ac9385671317d3fe4 \
--hash=sha256:bd46e5251ba5784438f9f2c64c73c09d138f5b292390c3e1014f18cbd5d461e2 \
# via -r requirements/dev.in
selenium==3.141.0 \
--hash=sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c \
--hash=sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d \
# via -r requirements/dev.in, selenium-wire
seqdiag==2.0.0 \
--hash=sha256:3167f16b4d15f3cd20de302fa600c96e4a50c92dae873bcbcf136c7588eeaa48 \
--hash=sha256:93ebc7a0c6b56b6ba0d1e36863c5749f03e82c487b6d1e6f1103b4219323f24c \
Expand Down Expand Up @@ -490,7 +498,7 @@ typing-extensions==3.7.4.3 \
urllib3==1.25.10 \
--hash=sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a \
--hash=sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461 \
# via requests
# via requests, selenium
virtualenv==20.0.31 \
--hash=sha256:43add625c53c596d38f971a465553f6318decc39d98512bc100fa1b1e839c8dc \
--hash=sha256:e0305af10299a7fb0d69393d8f04cb2965dda9351140d11ac8db4e5e3970451b \
Expand Down
6 changes: 3 additions & 3 deletions src/gafaelfawr/templates/new_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@
{% block content %}
<h2>Download Token</h2>

<form action="" method="post" novalidate>
<form action="" method="post" id="create-token" novalidate>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
<h3>Token Scopes</h3>

{% for scope in scopes %}
<div class="form-group row">
<div class="form-group row qa-token-scope">
<div class="col-md-4">
<div class="form-check">
{{ form[scope](class="form-check-input") }}
{{ form[scope].label(class="form-check-label") }}
</div>
</div>
<div class="col-md-8 text-dark">
<div class="col-md-8 text-dark qa-scope-description">
{{ form[scope].description }}
</div>
</div>
Expand Down
22 changes: 11 additions & 11 deletions src/gafaelfawr/templates/tokens.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<div class="row">
<div class="col-md-12">
<div class="float-right">
<a class="btn btn-sm btn-primary" href="/auth/tokens/new">
<a class="btn btn-sm btn-primary" id="new-token" href="/auth/tokens/new">
Generate new token
</a>
</div>
Expand All @@ -35,30 +35,30 @@ <h2>Personal access tokens</h2>
<div class="col-md-12">
<div class="list-group">
{% for token in tokens %} {% set form=forms[token.key] %}
<form action="/auth/tokens/{{ token.key }}" method="POST"
novalidate>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
{{ form.method_(value="DELETE") }}
<div class="list-group-item">
<div class="list-group-item qa-token-row">
<form action="/auth/tokens/{{ token.key }}" method="POST"
novalidate>
<input type="hidden" name="_csrf" value="{{ csrf_token }}" />
{{ form.method_(value="DELETE") }}
<div class="float-right">
<button class="btn btn-sm btn-danger"
<button class="btn btn-sm btn-danger qa-revoke-token"
aria-haspopup="dialog" type="submit">Revoke</button>
</div>
<span class="token-description">
<strong>
<strong class="token-link">
<a href="/auth/tokens/{{ token.key }}">{{ token.key }}</a>
</strong>
<span>
<br/>
Scope: <em>
<span class="text-dark" aria-label="Token scope">
<span class="text-dark qa-token-scope" aria-label="Token scope">
{{ token.scope }}
</span>
</em>
</span>
</span>
</div>
</form>
</form>
</div>
{% endfor %}
</div>
</div>
Expand Down
33 changes: 28 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,42 @@

from tests.setup import SetupTest
from tests.support.app import create_test_app
from tests.support.selenium import selenium_driver

if TYPE_CHECKING:
from pathlib import Path
from typing import Any, Awaitable, Callable, Iterable, List, Optional
from typing import (
Any,
Awaitable,
Callable,
Iterable,
Iterator,
List,
Optional,
)

from aiohttp import web
from aiohttp.pytest_plugin.test_utils import TestClient
from seleniumwire import webdriver

from gafaelfawr.config import OIDCClient
from tests.setup import SetupTestCallable


@pytest.fixture(scope="session")
def driver() -> Iterator[webdriver.Chrome]:
"""Create a driver for Selenium testing.
Returns
-------
driver : `selenium.webdriver.Chrome`
The web driver to use in Selenium tests.
"""
driver = selenium_driver()
yield driver
driver.quit()


@pytest.fixture
def responses() -> Iterable[aioresponses]:
"""Create an aioresponses context manager.
Expand Down Expand Up @@ -114,10 +138,9 @@ async def _create_test_setup(
oidc_clients=oidc_clients,
**settings,
)
test_client = None
if client:
client = await aiohttp_client(app)
return SetupTest(app, responses, client)
else:
return SetupTest(app, responses)
test_client = await aiohttp_client(app)
return SetupTest(app, responses, test_client)

return _create_test_setup
Empty file added tests/pages/__init__.py
Empty file.
39 changes: 39 additions & 0 deletions tests/pages/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Base page model for Selenium tests."""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import List, Union

from selenium.webdriver.remote.webelement import WebElement
from seleniumwire import webdriver


class BaseFinder:
def __init__(self, root: Union[webdriver.Chrome, WebElement]) -> None:
self.root = root

def find_element_by_class_name(self, name: str) -> WebElement:
return self.root.find_element_by_class_name(name)

def find_elements_by_class_name(self, name: str) -> List[WebElement]:
return self.root.find_elements_by_class_name(name)

def find_element_by_id(self, id_: str) -> WebElement:
return self.root.find_element_by_id(id_)


class BasePage(BaseFinder):
def __init__(self, root: webdriver.Chrome) -> None:
self.root = root

@property
def page_source(self) -> str:
return self.root.page_source


class BaseElement(BaseFinder):
def __init__(self, root: WebElement) -> None:
self.root = root
84 changes: 84 additions & 0 deletions tests/pages/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Page models for token-related pages."""

from __future__ import annotations

import re
from typing import TYPE_CHECKING

from tests.pages.base import BaseElement, BasePage
from tests.support.selenium import run

if TYPE_CHECKING:
from typing import List, Optional

from selenium.webdriver.remote.webelement import WebElement


class NewTokenPage(BasePage):
@property
def form(self) -> WebElement:
return self.find_element_by_id("create-token")

@property
def scopes(self) -> List[ScopeRow]:
return [
ScopeRow(e)
for e in self.find_elements_by_class_name("qa-token-scope")
]

async def submit(self) -> None:
button = self.form.find_element_by_id("submit")
await run(button.click)


class TokensPage(BasePage):
@property
def new_token(self) -> Optional[str]:
alert = self.find_elements_by_class_name("alert")
if not alert:
return None
match = re.search("Token: ([^ ]+)", alert[0].text)
if match:
return match.group(1)
else:
return None

@property
def tokens(self) -> List[TokenRow]:
return [
TokenRow(e)
for e in self.find_elements_by_class_name("qa-token-row")
]

async def click_create_token(self) -> None:
button = self.find_element_by_id("new-token")
await run(button.click)


class ScopeRow(BaseElement):
@property
def checkbox(self) -> WebElement:
return self.find_element_by_class_name("form-check-input")

@property
def description(self) -> str:
return self.find_element_by_class_name("qa-scope-description").text

@property
def label(self) -> str:
return self.find_element_by_class_name("form-check-label").text


class TokenRow(BaseElement):
@property
def key(self) -> str:
return self.find_element_by_class_name("token-link").text

@property
def link(self) -> str:
token_link = self.find_element_by_class_name("token-link")
return token_link.find_element_by_tag_name("a").get_attribute("href")

@property
def scope(self) -> str:
return self.find_element_by_class_name("qa-token-scope").text
Empty file added tests/selenium/__init__.py
Empty file.
47 changes: 47 additions & 0 deletions tests/selenium/tokens_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Selenium tests for ``/auth/tokens``."""

from __future__ import annotations

from typing import TYPE_CHECKING

from gafaelfawr.session import SessionHandle
from tests.pages.tokens import NewTokenPage, TokensPage
from tests.support.selenium import run

if TYPE_CHECKING:
from seleniumwire import webdriver

from tests.setup import SetupTestCallable


async def test_create_token(
create_test_setup: SetupTestCallable, driver: webdriver.Chrome
) -> None:
setup = await create_test_setup()
token = setup.create_token(scope="read:all")
driver.header_overrides = {"X-Auth-Request-Token": token.encoded}

tokens_url = str(setup.client.make_url("/auth/tokens"))
await run(lambda: driver.get(tokens_url))

tokens_page = TokensPage(driver)
assert tokens_page.tokens == []
await tokens_page.click_create_token()

new_tokens_page = NewTokenPage(driver)
assert len(new_tokens_page.scopes) == 1
scope = new_tokens_page.scopes[0]
assert scope.label == "read:all"
assert scope.description == "can read everything"
assert not scope.checkbox.is_selected()
scope.checkbox.click()
await new_tokens_page.submit()

tokens_page = TokensPage(driver)
assert tokens_page.new_token
session_handle = SessionHandle.from_str(tokens_page.new_token)
assert len(tokens_page.tokens) == 1
token_row = tokens_page.tokens[0]
assert token_row.key == session_handle.key
assert token_row.link.endswith(f"/auth/tokens/{session_handle.key}")
assert token_row.scope == "read:all"
Loading

0 comments on commit c76642e

Please sign in to comment.