This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/main by this push:
new fe0d7e2 Add some e2e token tests
fe0d7e2 is described below
commit fe0d7e21a0f2fa3cdcde7cdcd0bf909851bb136a
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jan 22 14:47:12 2026 +0000
Add some e2e token tests
---
atr/storage/writers/tokens.py | 23 +++++-----
pyproject.toml | 1 +
tests/e2e/tokens/__init__.py | 16 +++++++
tests/e2e/tokens/conftest.py | 44 +++++++++++++++++++
tests/e2e/tokens/helpers.py | 34 +++++++++++++++
tests/e2e/tokens/test_get.py | 47 +++++++++++++++++++++
tests/e2e/tokens/test_post.py | 98 +++++++++++++++++++++++++++++++++++++++++++
7 files changed, 252 insertions(+), 11 deletions(-)
diff --git a/atr/storage/writers/tokens.py b/atr/storage/writers/tokens.py
index 3418722..f6b45ed 100644
--- a/atr/storage/writers/tokens.py
+++ b/atr/storage/writers/tokens.py
@@ -20,6 +20,7 @@ from __future__ import annotations
import datetime
import hashlib
+from typing import Final
import sqlmodel
@@ -31,6 +32,9 @@ import atr.models.sql as sql
import atr.storage as storage
import atr.util as util
+# TODO: Check that this is known and that its emails are correctly discarded
+NOREPLY_EMAIL_ADDRESS: Final[str] = "[email protected]"
+
class GeneralPublic:
def __init__(
@@ -59,6 +63,8 @@ class FoundationCommitter(GeneralPublic):
async def add_token(
self, uid: str, token_hash: str, created: datetime.datetime, expires:
datetime.datetime, label: str | None
) -> sql.PersonalAccessToken:
+ if not label:
+ raise ValueError("Label is required")
pat = sql.PersonalAccessToken(
asfuid=uid,
token_hash=token_hash,
@@ -68,19 +74,16 @@ class FoundationCommitter(GeneralPublic):
)
self.__data.add(pat)
await self.__data.commit()
- # inform user
message = mail.Message(
- email_sender="[email protected]",
+ email_sender=NOREPLY_EMAIL_ADDRESS,
email_recipient=f"{uid}@apache.org",
subject="New API Token Created",
- body=f"A new API token '{label or 'unlabeled'}' was created for
your account. "
+ body=f"A new API token called '{label}' was created for your
account. "
"If you did not create this token, please revoke it immediately.",
)
if util.is_dev_environment():
- # Pretend to send the mail
log.info("Dev environment detected, pretending to send mail")
else:
- # Send the mail
await mail.send(message)
return pat
@@ -98,19 +101,17 @@ class FoundationCommitter(GeneralPublic):
asf_uid=self.__asf_uid,
token_id=token_id,
)
- # inform user
+ label = pat.label or "[unlabeled]"
message = mail.Message(
- email_sender="[email protected]",
+ email_sender=NOREPLY_EMAIL_ADDRESS,
email_recipient=f"{self.__asf_uid}@apache.org",
subject="Deleted API Token",
- body="An API token was deleted from your account. "
- "If you did not delete any tokens, please checkl your account
immediately.",
+ body=f"An API token called '{label}' was deleted from your
account. "
+ "If you did not delete this token, please check your account
immediately.",
)
if util.is_dev_environment():
- # Pretend to send the mail
log.info("Dev environment detected, pretending to send mail")
else:
- # Send the mail
await mail.send(message)
async def issue_jwt(self, pat_text: str) -> str:
diff --git a/pyproject.toml b/pyproject.toml
index 4a8466b..e322d7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -127,6 +127,7 @@ extend-exclude = [
ignore = [
"S101", # assert
"S104", # interfaces
+ "S105", # hardcoded password
"S106", # password keyword
"S314", # old expat concerns
"S603", # subprocess.run
diff --git a/tests/e2e/tokens/__init__.py b/tests/e2e/tokens/__init__.py
new file mode 100644
index 0000000..13a8339
--- /dev/null
+++ b/tests/e2e/tokens/__init__.py
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
diff --git a/tests/e2e/tokens/conftest.py b/tests/e2e/tokens/conftest.py
new file mode 100644
index 0000000..e8c5cd4
--- /dev/null
+++ b/tests/e2e/tokens/conftest.py
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import e2e.helpers as helpers
+import e2e.tokens.helpers as token_helpers
+import pytest
+
+if TYPE_CHECKING:
+ from collections.abc import Generator
+
+ from playwright.sync_api import Page
+
+
[email protected]
+def page_tokens(page: Page) -> Generator[Page]:
+ helpers.log_in(page)
+ helpers.visit(page, token_helpers.TOKENS_PATH)
+ yield page
+
+
[email protected]
+def page_tokens_clean(page: Page) -> Generator[Page]:
+ helpers.log_in(page)
+ helpers.visit(page, token_helpers.TOKENS_PATH)
+ token_helpers.delete_token_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ yield page
diff --git a/tests/e2e/tokens/helpers.py b/tests/e2e/tokens/helpers.py
new file mode 100644
index 0000000..a221b69
--- /dev/null
+++ b/tests/e2e/tokens/helpers.py
@@ -0,0 +1,34 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+from typing import Final
+
+from playwright.sync_api import Page
+
+TOKEN_LABEL_FOR_TESTING: Final[str] = "e2e-test-token"
+TOKENS_PATH: Final[str] = "/tokens"
+
+
+def delete_token_by_label(page: Page, label: str) -> None:
+ row = get_token_row_by_label(page, label)
+ if row.count() > 0:
+ row.get_by_role("button", name="Delete").click()
+ page.wait_for_load_state()
+
+
+def get_token_row_by_label(page: Page, label: str) -> Page:
+ return page.locator(f'tr:has(td:text-is("{label}"))')
diff --git a/tests/e2e/tokens/test_get.py b/tests/e2e/tokens/test_get.py
new file mode 100644
index 0000000..7e269b3
--- /dev/null
+++ b/tests/e2e/tokens/test_get.py
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+from playwright.sync_api import Page, expect
+
+
+def test_tokens_page_has_generate_jwt_button(page_tokens: Page) -> None:
+ button = page_tokens.get_by_role("button", name="Generate JWT")
+ expect(button).to_be_visible()
+
+
+def test_tokens_page_has_generate_token_button(page_tokens: Page) -> None:
+ button = page_tokens.get_by_role("button", name="Generate token")
+ expect(button).to_be_visible()
+
+
+def test_tokens_page_has_jwt_heading(page_tokens: Page) -> None:
+ heading = page_tokens.get_by_role("heading", name="JSON Web Token (JWT)")
+ expect(heading).to_be_visible()
+
+
+def test_tokens_page_has_label_input(page_tokens: Page) -> None:
+ label_input = page_tokens.locator('input[name="label"]')
+ expect(label_input).to_be_visible()
+
+
+def test_tokens_page_has_pats_heading(page_tokens: Page) -> None:
+ heading = page_tokens.get_by_role("heading", name="Personal Access Tokens
(PATs)")
+ expect(heading).to_be_visible()
+
+
+def test_tokens_page_loads(page_tokens: Page) -> None:
+ expect(page_tokens).to_have_title("Tokens ~ ATR")
diff --git a/tests/e2e/tokens/test_post.py b/tests/e2e/tokens/test_post.py
new file mode 100644
index 0000000..96d1b67
--- /dev/null
+++ b/tests/e2e/tokens/test_post.py
@@ -0,0 +1,98 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you 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.
+
+import e2e.tokens.helpers as token_helpers
+from playwright.sync_api import Page, expect
+
+
+def test_create_token_with_label(page_tokens_clean: Page) -> None:
+ page = page_tokens_clean
+ label_input = page.locator('input[name="label"]')
+ label_input.fill(token_helpers.TOKEN_LABEL_FOR_TESTING)
+ page.get_by_role("button", name="Generate token").click()
+ page.wait_for_load_state()
+
+ success_message = page.locator(".flash-message.flash-success")
+ expect(success_message).to_be_visible()
+ expect(success_message).to_contain_text("Your new token")
+
+ token_row = token_helpers.get_token_row_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ expect(token_row).to_be_visible()
+
+ token_helpers.delete_token_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+
+
+def test_create_token_without_label_shows_error(page_tokens_clean: Page) ->
None:
+ page = page_tokens_clean
+ page.get_by_role("button", name="Generate token").click()
+ page.wait_for_load_state()
+
+ error_message = page.locator(".flash-message.flash-error")
+ expect(error_message).to_be_visible()
+ expect(error_message).to_contain_text("Label is required")
+
+
+def test_delete_token_shows_success(page_tokens_clean: Page) -> None:
+ page = page_tokens_clean
+
+ label_input = page.locator('input[name="label"]')
+ label_input.fill(token_helpers.TOKEN_LABEL_FOR_TESTING)
+ page.get_by_role("button", name="Generate token").click()
+ page.wait_for_load_state()
+
+ token_row = token_helpers.get_token_row_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ expect(token_row).to_be_visible()
+
+ token_row.get_by_role("button", name="Delete").click()
+ page.wait_for_load_state()
+
+ success_message = page.locator(".flash-message.flash-success")
+ expect(success_message).to_be_visible()
+ expect(success_message).to_contain_text("Token deleted successfully")
+
+ token_row = token_helpers.get_token_row_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ expect(token_row).to_have_count(0)
+
+
+def test_token_table_shows_created_date(page_tokens_clean: Page) -> None:
+ page = page_tokens_clean
+
+ label_input = page.locator('input[name="label"]')
+ label_input.fill(token_helpers.TOKEN_LABEL_FOR_TESTING)
+ page.get_by_role("button", name="Generate token").click()
+ page.wait_for_load_state()
+
+ token_row = token_helpers.get_token_row_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ created_cell = token_row.locator("td").nth(1)
+ expect(created_cell).not_to_be_empty()
+
+ token_helpers.delete_token_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+
+
+def test_token_table_shows_expires_date(page_tokens_clean: Page) -> None:
+ page = page_tokens_clean
+
+ label_input = page.locator('input[name="label"]')
+ label_input.fill(token_helpers.TOKEN_LABEL_FOR_TESTING)
+ page.get_by_role("button", name="Generate token").click()
+ page.wait_for_load_state()
+
+ token_row = token_helpers.get_token_row_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
+ expires_cell = token_row.locator("td").nth(2)
+ expect(expires_cell).not_to_be_empty()
+
+ token_helpers.delete_token_by_label(page,
token_helpers.TOKEN_LABEL_FOR_TESTING)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]