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]

Reply via email to