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 50ce059 Migrate browser tests to a dedicated test committee
50ce059 is described below
commit 50ce0593aa9deea9c03e8173d717b61f3208ab15
Author: Sean B. Palmer <[email protected]>
AuthorDate: Tue Oct 14 09:49:41 2025 +0100
Migrate browser tests to a dedicated test committee
---
...=> 0924BA4B875A1A472D147C1EB11EB02801756B9E.asc | 0
Makefile | 4 +-
atr/blueprints/admin/admin.py | 31 ++++
atr/config.py | 4 +
atr/principal.py | 38 ++++-
atr/route.py | 17 +-
atr/routes/projects.py | 6 +-
atr/routes/root.py | 30 +++-
atr/server.py | 36 ++++
atr/storage/writers/keys.py | 59 ++++++-
atr/user.py | 14 +-
.../68FF2E20F02B070D73D416188DE8CC167FE2663A.asc | 42 +++++
playwright/apache-test-0.2/apache-test-0.2.tar.gz | Bin 0 -> 4391 bytes
.../apache-test-0.2/apache-test-0.2.tar.gz.asc | 8 +
.../apache-test-0.2/apache-test-0.2.tar.gz.sha512 | 1 +
.../apache-tooling-test-example-0.2.tar.gz | Bin 4432 -> 0 bytes
.../apache-tooling-test-example-0.2.tar.gz.asc | 8 -
.../apache-tooling-test-example-0.2.tar.gz.sha512 | 1 -
playwright/mk.sh | 11 +-
playwright/test.py | 182 ++++++++++++---------
20 files changed, 372 insertions(+), 120 deletions(-)
diff --git a/playwright/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
b/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
similarity index 100%
rename from playwright/0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
rename to 0924BA4B875A1A472D147C1EB11EB02801756B9E.asc
diff --git a/Makefile b/Makefile
index 92e68ca..1462618 100644
--- a/Makefile
+++ b/Makefile
@@ -79,8 +79,8 @@ serve:
atr.server:app --debug --reload
serve-local:
- APP_HOST=localhost.apache.org:8080 LOCAL_DEBUG=1
SECRET_KEY=insecure-local-key \
- SSH_HOST=127.0.0.1 uv run hypercorn --bind $(BIND) \
+ APP_HOST=localhost.apache.org:8080 SECRET_KEY=insecure-local-key \
+ ALLOW_TESTS=1 SSH_HOST=127.0.0.1 uv run hypercorn --bind $(BIND) \
--keyfile localhost.apache.org+3-key.pem --certfile
localhost.apache.org+3.pem \
atr.server:app --debug --reload
diff --git a/atr/blueprints/admin/admin.py b/atr/blueprints/admin/admin.py
index adf81bb..69fe8fd 100644
--- a/atr/blueprints/admin/admin.py
+++ b/atr/blueprints/admin/admin.py
@@ -266,6 +266,37 @@ async def admin_data(model: str = "Committee") -> str:
)
[email protected]("/delete-test-openpgp-keys", methods=["GET", "POST"])
+async def admin_delete_test_openpgp_keys() -> quart.Response |
response.Response:
+ """Delete all test user OpenPGP keys and their links."""
+ if not config.get().ALLOW_TESTS:
+ raise base.ASFQuartException("Test key deletion not enabled",
errorcode=404)
+
+ test_uid = "test"
+
+ if quart.request.method != "POST":
+ empty_form = await forms.Empty.create_form()
+ return quart.Response(
+ f"""
+<form method="post">
+ <button type="submit">Delete all OpenPGP keys for {test_uid} user</button>
+ {empty_form.hidden_tag()}
+</form>
+""",
+ mimetype="text/html",
+ )
+
+ # This is a POST request
+ await util.validate_empty_form()
+
+ async with storage.write() as write:
+ wafc = write.as_foundation_committer()
+ outcome = await wafc.keys.test_user_delete_all(test_uid)
+ outcome.result_or_raise()
+
+ return quart.redirect("/keys")
+
+
@admin.BLUEPRINT.route("/delete-committee-keys", methods=["GET", "POST"])
async def admin_delete_committee_keys() -> str | response.Response:
form = await DeleteCommitteeKeysForm.create_form()
diff --git a/atr/config.py b/atr/config.py
index ca6b0e5..a80cdea 100644
--- a/atr/config.py
+++ b/atr/config.py
@@ -43,6 +43,7 @@ def _config_secrets(key: str, state_dir: str, default: str |
None = None, cast:
class AppConfig:
+ ALLOW_TESTS = decouple.config("ALLOW_TESTS", default=False, cast=bool)
APP_HOST = decouple.config("APP_HOST", default="localhost")
SSH_HOST = decouple.config("SSH_HOST", default="0.0.0.0")
SSH_PORT = decouple.config("SSH_PORT", default=2222, cast=int)
@@ -133,6 +134,9 @@ def get() -> type[AppConfig]:
except KeyError:
exit("Error: Invalid <mode>. Expected values [Debug, Production,
Profiling].")
+ if config.ALLOW_TESTS and (get_mode() != Mode.Debug):
+ raise RuntimeError("ALLOW_TESTS can only be enabled in Debug mode")
+
absolute_paths = [
(config.PROJECT_ROOT, "PROJECT_ROOT"),
(config.STATE_DIR, "STATE_DIR"),
diff --git a/atr/principal.py b/atr/principal.py
index 8ad1d5d..ccfad52 100644
--- a/atr/principal.py
+++ b/atr/principal.py
@@ -250,10 +250,15 @@ class AuthoriserASFQuart:
if not isinstance(asfquart_session, session.ClientSession):
# Defense in depth runtime check, already validated by the type
checker
raise AuthenticationError("ASFQuart session is not a
ClientSession")
+
+ committees = frozenset(asfquart_session.committees)
+ projects = frozenset(asfquart_session.projects)
+ committees, projects = _augment_test_membership(committees, projects)
+
# We do not check that the ASF UID is the same as the one in the
session
# It is the caller's responsibility to ensure this
- self.__cache.member_of[asf_uid] =
frozenset(asfquart_session.committees)
- self.__cache.participant_of[asf_uid] =
frozenset(asfquart_session.projects)
+ self.__cache.member_of[asf_uid] = committees
+ self.__cache.participant_of[asf_uid] = projects
self.__cache.last_refreshed = int(time.time())
@@ -279,11 +284,26 @@ class AuthoriserLDAP:
async def cache_refresh(self, asf_uid: str) -> None:
if not self.__cache.outdated():
return
+
+ if config.get().ALLOW_TESTS and (asf_uid == "test"):
+ # The test user does not exist in LDAP, so we hardcode their data
+ committees = frozenset({"test"})
+ projects = frozenset({"test"})
+ self.__cache.member_of[asf_uid] = committees
+ self.__cache.participant_of[asf_uid] = projects
+ self.__cache.last_refreshed = int(time.time())
+ return
+
try:
c = Committer(asf_uid)
await asyncio.to_thread(c.verify)
- self.__cache.member_of[asf_uid] = frozenset(c.pmcs)
- self.__cache.participant_of[asf_uid] = frozenset(c.projects)
+
+ committees = frozenset(c.pmcs)
+ projects = frozenset(c.projects)
+ committees, projects = _augment_test_membership(committees,
projects)
+
+ self.__cache.member_of[asf_uid] = committees
+ self.__cache.participant_of[asf_uid] = projects
self.__cache.last_refreshed = int(time.time())
except CommitterError as e:
raise AuthenticationError(f"Failed to verify committer: {e}") from
e
@@ -371,3 +391,13 @@ class Authorisation(AsyncObject):
if self.__asf_uid is None:
return frozenset()
return self.__authoriser.member_of(self.__asf_uid)
+
+
+def _augment_test_membership(
+ committees: frozenset[str],
+ projects: frozenset[str],
+) -> tuple[frozenset[str], frozenset[str]]:
+ if config.get().ALLOW_TESTS:
+ committees = committees.union({"test"})
+ projects = projects.union({"test"})
+ return committees, projects
diff --git a/atr/route.py b/atr/route.py
index ab7bac1..9b8ff19 100644
--- a/atr/route.py
+++ b/atr/route.py
@@ -222,11 +222,7 @@ class CommitterSession:
self, route: CommitterRouteHandler[R], success: str | None = None,
error: str | None = None, **kwargs: Any
) -> response.Response:
"""Redirect to a route with a success or error message."""
- if success is not None:
- await quart.flash(success, "success")
- elif error is not None:
- await quart.flash(error, "error")
- return quart.redirect(util.as_url(route, **kwargs))
+ return await redirect(route, success, error, **kwargs)
async def release(
self,
@@ -510,6 +506,17 @@ def public(
return decorator
+async def redirect[R](
+ route: RouteHandler[R], success: str | None = None, error: str | None =
None, **kwargs: Any
+) -> response.Response:
+ """Redirect to a route with a success or error message."""
+ if success is not None:
+ await quart.flash(success, "success")
+ elif error is not None:
+ await quart.flash(error, "error")
+ return quart.redirect(util.as_url(route, **kwargs))
+
+
def _authentication_failed() -> NoReturn:
"""Handle authentication failure with an exception."""
# NOTE: This is a separate function to fix a problem with analysis flow in
mypy
diff --git a/atr/routes/projects.py b/atr/routes/projects.py
index 80270a7..ccb93c1 100644
--- a/atr/routes/projects.py
+++ b/atr/routes/projects.py
@@ -27,6 +27,7 @@ from typing import TYPE_CHECKING, Any
import asfquart.base as base
import quart
+import atr.config as config
import atr.db as db
import atr.db.interaction as interaction
import atr.forms as forms
@@ -291,13 +292,16 @@ async def select(session: route.CommitterSession) -> str:
if session.uid:
async with db.session() as data:
# TODO: Move this filtering logic somewhere else
+ # The ALLOW_TESTS line allows test projects to be shown
+ conf = config.get()
all_projects = await data.project(status=sql.ProjectStatus.ACTIVE,
_committee=True).all()
user_projects = [
p
for p in all_projects
if p.committee
and (
- (session.uid in p.committee.committee_members)
+ (conf.ALLOW_TESTS and (p.committee.name == "test"))
+ or (session.uid in p.committee.committee_members)
or (session.uid in p.committee.committers)
or (session.uid in p.committee.release_managers)
)
diff --git a/atr/routes/root.py b/atr/routes/root.py
index 41f8c5e..e5f340d 100644
--- a/atr/routes/root.py
+++ b/atr/routes/root.py
@@ -21,11 +21,13 @@ import pathlib
from typing import Final
import aiofiles
+import asfquart.base as base
import asfquart.session
import htpy
-import quart.wrappers.response as response
+import quart.wrappers.response as quart_response
import sqlalchemy.orm as orm
import sqlmodel
+import werkzeug.wrappers.response as response
import atr.config as config
import atr.db as db
@@ -74,7 +76,7 @@ async def about(session: route.CommitterSession) -> str:
@route.public("/")
-async def index(session: route.CommitterSession | None) -> response.Response |
str:
+async def index(session: route.CommitterSession | None) ->
quart_response.Response | str:
"""Show public info or an entry portal for participants."""
session_data = await asfquart.session.read()
if session_data:
@@ -149,11 +151,11 @@ async def index(session: route.CommitterSession | None)
-> response.Response | s
@route.public("/miscellaneous/resolved.json")
-async def resolved_json(session: route.CommitterSession | None) ->
response.Response:
+async def resolved_json(session: route.CommitterSession | None) ->
quart_response.Response:
json_path = pathlib.Path(config.get().PROJECT_ROOT) / "atr" / "static" /
"json" / "resolved.json"
async with aiofiles.open(json_path) as f:
content = await f.read()
- return response.Response(content, mimetype="application/json")
+ return quart_response.Response(content, mimetype="application/json")
@route.public("/policies")
@@ -161,6 +163,26 @@ async def policies(session: route.CommitterSession | None)
-> str:
return await template.blank("Policies", content=_POLICIES)
[email protected]("/test-login")
+async def test_login(session: route.CommitterSession | None) ->
response.Response:
+ if not config.get().ALLOW_TESTS:
+ raise base.ASFQuartException("Test login not enabled", errorcode=404)
+
+ session_data = {
+ "uid": "test",
+ "fullname": "Test User",
+ "committees": ["test"],
+ "projects": ["test"],
+ "isMember": False,
+ "isChair": False,
+ "isRole": False,
+ "metadata": {},
+ }
+
+ asfquart.session.write(session_data)
+ return await route.redirect(index)
+
+
@route.committer("/todo", methods=["POST"])
async def todo(session: route.CommitterSession) -> str:
"""POST target for development."""
diff --git a/atr/server.py b/atr/server.py
index fa78a06..4a7f79c 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -19,6 +19,7 @@
import asyncio
import contextlib
+import datetime
import os
import queue
from collections.abc import Iterable
@@ -44,6 +45,7 @@ import atr.db.interaction as interaction
import atr.filters as filters
import atr.log as log
import atr.manager as manager
+import atr.models.sql as sql
import atr.preload as preload
import atr.ssh as ssh
import atr.svn.pubsub as pubsub
@@ -156,6 +158,8 @@ def app_setup_lifecycle(app: base.QuartApp) -> None:
worker_manager = manager.get_worker_manager()
await worker_manager.start()
+ await initialise_test_environment()
+
conf = config.get()
pubsub_url = conf.PUBSUB_URL
pubsub_user = conf.PUBSUB_USER
@@ -303,6 +307,38 @@ def create_app(app_config: type[config.AppConfig]) ->
base.QuartApp:
return app
+async def initialise_test_environment() -> None:
+ if not config.get().ALLOW_TESTS:
+ return
+
+ async with db.session() as data:
+ test_committee = await data.committee(name="test").get()
+ if not test_committee:
+ test_committee = sql.Committee(
+ name="test",
+ full_name="Test Committee",
+ is_podling=False,
+ committee_members=["test"],
+ committers=["test"],
+ release_managers=["test"],
+ )
+ data.add(test_committee)
+ await data.commit()
+
+ test_project = await data.project(name="test").get()
+ if not test_project:
+ test_project = sql.Project(
+ name="test",
+ full_name="Apache Test",
+ status=sql.ProjectStatus.ACTIVE,
+ committee_name="test",
+ created=datetime.datetime.now(datetime.UTC),
+ created_by="test",
+ )
+ data.add(test_project)
+ await data.commit()
+
+
def main() -> None:
"""Quart debug server"""
global app
diff --git a/atr/storage/writers/keys.py b/atr/storage/writers/keys.py
index b8f4053..942c080 100644
--- a/atr/storage/writers/keys.py
+++ b/atr/storage/writers/keys.py
@@ -32,7 +32,9 @@ import aiofiles.os
import pgpy
import pgpy.constants as constants
import sqlalchemy.dialects.sqlite as sqlite
+import sqlmodel
+import atr.config as config
import atr.db as db
import atr.log as log
import atr.models.sql as sql
@@ -92,7 +94,11 @@ class FoundationCommitter(GeneralPublic):
return await self.__ensure_one(key_file_text, associate=False)
def keyring_fingerprint_model(
- self, keyring: pgpy.PGPKeyring, fingerprint: str, ldap_data: dict[str,
str]
+ self,
+ keyring: pgpy.PGPKeyring,
+ fingerprint: str,
+ ldap_data: dict[str, str],
+ original_key_block: str | None = None,
) -> sql.PublicSigningKey | None:
with keyring.key(fingerprint) as key:
if not key.is_primary:
@@ -113,6 +119,9 @@ class FoundationCommitter(GeneralPublic):
else:
raise ValueError(f"Key size is not an integer:
{type(key_size)}, {key_size}")
+ # Use the original key block if available
+ ascii_armored = original_key_block if original_key_block else
str(key)
+
return sql.PublicSigningKey(
fingerprint=str(key.fingerprint).lower(),
algorithm=key.key_algorithm.value,
@@ -123,7 +132,7 @@ class FoundationCommitter(GeneralPublic):
primary_declared_uid=uids[0],
secondary_declared_uids=uids[1:],
apache_uid=asf_uid,
- ascii_armored_key=str(key),
+ ascii_armored_key=ascii_armored,
)
async def keys_file_text(self, committee_name: str) -> str:
@@ -169,6 +178,30 @@ class FoundationCommitter(GeneralPublic):
key_blocks_str=key_blocks_str,
)
+ async def test_user_delete_all(self, test_uid: str) ->
outcome.Outcome[int]:
+ """Delete all OpenPGP keys and their links for a test user."""
+ if not config.get().ALLOW_TESTS:
+ return outcome.Error(storage.AccessError("Test key deletion not
enabled"))
+
+ try:
+ test_user_keys = await
self.__data.public_signing_key(apache_uid=test_uid).all()
+
+ deleted_count = 0
+ for key in test_user_keys:
+ keylinks_query =
sqlmodel.select(sql.KeyLink).where(sql.KeyLink.key_fingerprint ==
key.fingerprint)
+ keylinks_result = await self.__data.execute(keylinks_query)
+ keylinks = keylinks_result.all()
+ for keylink_row in keylinks:
+ await self.__data.delete(keylink_row[0])
+
+ await self.__data.delete(key)
+ deleted_count += 1
+
+ await self.__data.commit()
+ return outcome.Result(deleted_count)
+ except Exception as e:
+ return outcome.Error(e)
+
def __block_model(self, key_block: str, ldap_data: dict[str, str]) ->
types.Key:
# This cache is only held for the session
if key_block in self.__key_block_models_cache:
@@ -186,7 +219,9 @@ class FoundationCommitter(GeneralPublic):
key = None
for fingerprint in fingerprints:
try:
- key_model = self.keyring_fingerprint_model(keyring,
fingerprint, ldap_data)
+ key_model = self.keyring_fingerprint_model(
+ keyring, fingerprint, ldap_data,
original_key_block=key_block
+ )
if key_model is None:
# Was not a primary key, so skip it
continue
@@ -292,12 +327,16 @@ and was published by the committee.\
test_key_uids = [
"Apache Tooling (For test use only)
<[email protected]>",
]
- is_admin = user.is_admin(self.__asf_uid)
- if (uids == test_key_uids) and is_admin:
+
+ if uids == test_key_uids:
# Allow the test key
- # TODO: We should fix the test key, not add an exception for it
- # But the admin check probably makes this safe enough
- return self.__asf_uid
+ if config.get().ALLOW_TESTS and (self.__asf_uid == "test"):
+ # TODO: "test" is already an admin user
+ # But we want to narrow that down to only actions like this
+ # TODO: Add include_test: bool to user.is_admin?
+ return "test"
+ if user.is_admin(self.__asf_uid):
+ return self.__asf_uid
# Regular data
emails = []
@@ -456,7 +495,9 @@ class CommitteeParticipant(FoundationCommitter):
key_list = []
for fingerprint in fingerprints:
try:
- key_model = self.keyring_fingerprint_model(keyring,
fingerprint, ldap_data)
+ key_model = self.keyring_fingerprint_model(
+ keyring, fingerprint, ldap_data,
original_key_block=key_block
+ )
if key_model is None:
# Was not a primary key, so skip it
continue
diff --git a/atr/user.py b/atr/user.py
index 0e9e081..cfb1c1d 100644
--- a/atr/user.py
+++ b/atr/user.py
@@ -39,7 +39,12 @@ async def candidate_drafts(uid: str, user_projects:
list[sql.Project] | None = N
@functools.cache
def get_admin_users() -> set[str]:
- return set(config.get().ADMIN_USERS)
+ admin_users = set(config.get().ADMIN_USERS)
+ if config.get().ALLOW_TESTS:
+ # TODO: Just for debugging, but ideally we would do this in a targeted
way
+ # We need this, for example, for deleting releases
+ admin_users.add("test")
+ return admin_users
def is_admin(user_id: str | None) -> bool:
@@ -71,6 +76,13 @@ async def projects(uid: str, committee_only: bool = False,
super_project: bool =
for p in projects:
if p.committee is None:
continue
+
+ # Allow access to test project when ALLOW_TESTS is enabled
+ # This means that the Test project will show in the user interface
for everyone
+ if config.get().ALLOW_TESTS and (p.committee.name == "test"):
+ user_projects.append(p)
+ continue
+
if committee_only:
if uid in p.committee.committee_members:
user_projects.append(p)
diff --git a/playwright/68FF2E20F02B070D73D416188DE8CC167FE2663A.asc
b/playwright/68FF2E20F02B070D73D416188DE8CC167FE2663A.asc
new file mode 100644
index 0000000..942dfef
--- /dev/null
+++ b/playwright/68FF2E20F02B070D73D416188DE8CC167FE2663A.asc
@@ -0,0 +1,42 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: 68FF 2E20 F02B 070D 73D4 1618 8DE8 CC16 7FE2 663A
+Comment: Apache Tooling (For test use only) <apache-tooling@exam
+
+xjMEaO1PsBYJKwYBBAHaRw8BAQdApoMTlP31+p4iNHPXRsluuFJD7/n7ZfvbXTaW
+nuPqcNXCwBEEHxYKAIMFgmjtT7AFiQWkj70DCwkHCRCN6MwWf+JmOkcUAAAAAAAe
+ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmc62pYvwVHI/mS0pKwOQyF2
+4zTbGfhRAUmA5Lb0wiCA8wMVCggCm4ECHgkWIQRo/y4g8CsHDXPUFhiN6MwWf+Jm
+OgAAaCsA/juShbk2IlhDKf2KxwTMr88Ce/zDsw4+eiWtrthgBUeTAP4grngx+xez
+Ih3T0oHijFiRRFHeCrKtrD7qUYiRWZQrDc1DQXBhY2hlIFRvb2xpbmcgKEZvciB0
+ZXN0IHVzZSBvbmx5KSA8YXBhY2hlLXRvb2xpbmdAZXhhbXBsZS5pbnZhbGlkPsLA
+FAQTFgoAhgWCaO1PsAWJBaSPvQMLCQcJEI3ozBZ/4mY6RxQAAAAAAB4AIHNhbHRA
+bm90YXRpb25zLnNlcXVvaWEtcGdwLm9yZy/CVowGIUPB/Q2pB93lBODdG76rdB57
+tRtDUd1vvJTbAxUKCAKZAQKbgQIeCRYhBGj/LiDwKwcNc9QWGI3ozBZ/4mY6AAC2
+ggD/RzmgDOdwDbYa3yunGlaeNdXX9mKZr6l1ZZxPgxjCUUgA/iv14Q0jdlQTlnCE
+mU2KerEx3u4DKxTL3vSzrt7/YfYFzjMEaO1PsBYJKwYBBAHaRw8BAQdAOj8uLbY9
+uV5X5dHQouxpqNmBEiVL3yULoRGZIIcyjgzCwMUEGBYKATcFgmjtT7AFiQWkj70J
+EI3ozBZ/4mY6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdwLm9y
+Z/rQQDjxu84TK0fnjT6/eJfY7Ry9NZPUC4OnZ22u3H2eApugvqAEGRYKAG8Fgmjt
+T7AJEB4aFkanXZJrRxQAAAAAAB4AIHNhbHRAbm90YXRpb25zLnNlcXVvaWEtcGdw
+Lm9yZ3usYbMZaif3OJ191geiqbHhOJKxQAnKeYrTTRIBRtuSFiEEHnCR6AIvyq8N
+clBVHhoWRqddkmsAANfnAP9q5S4wzYCzBWXBMgT8GkGQ6qlozPKbDrLlSBByuPWx
+1AD/bQxhQOSOyoTl93gX+LEgzOl90eilDC36YcwlAAvPSQcWIQRo/y4g8CsHDXPU
+FhiN6MwWf+JmOgAAf1MBAM6fEH6DqxAej4v5JMSvhFK/VSMy9l74KEjdS4zQqGf+
+AP0b2hno+V/sKwsIZeLcT2WW8ymesnwcMlyJAKRawsGODs4zBGjtT7AWCSsGAQQB
+2kcPAQEHQOnskJXKy8myDOIN96kyCybjuusSj0UMKKOcKpKgCbjBwsDFBBgWCgE3
+BYJo7U+wBYkFpI+9CRCN6MwWf+JmOkcUAAAAAAAeACBzYWx0QG5vdGF0aW9ucy5z
+ZXF1b2lhLXBncC5vcmetuP08aKM3bgGpJSqcY0BFDIgHZVRzbhqkS6jW70k5rgKb
+gr6gBBkWCgBvBYJo7U+wCRCBsaC42dIMukcUAAAAAAAeACBzYWx0QG5vdGF0aW9u
+cy5zZXF1b2lhLXBncC5vcmdvv5LhZuUYSxJ371orAeZNemqPQFs2TVnNAkqcOvQl
+xhYhBKMvD8z/SMlPuXH5hYGxoLjZ0gy6AABPrQEA/68vHpMtFSLciL11wPdwlQya
+7IgXmd+1ex/fKd0lL8UBAKs6o8npS3TFMOdQ/lY77+2i4mQ8R/06LG7UqJlRA9YF
+FiEEaP8uIPArBw1z1BYYjejMFn/iZjoAAILfAQCTsMRBr0p26+mM9DyAfloTXjtC
+b64WedgkrliFylIEswEAhgoBH5hKugQuQoGGMAaCgfiQiDjzKmy5Qq5apdqdzgjO
+OARo7U+wEgorBgEEAZdVAQUBAQdA2SplsmuOmaNAaFJv5/6ZIU4fo2/tAhtiB4mZ
+IwtSkWoDAQgHwsAGBBgWCgB4BYJo7U+wBYkFpI+9CRCN6MwWf+JmOkcUAAAAAAAe
+ACBzYWx0QG5vdGF0aW9ucy5zZXF1b2lhLXBncC5vcmdlwwnRv1QM8Bh2keRKFRz4
+8KCfPtinCmGR/RXxuR3A+wKbjBYhBGj/LiDwKwcNc9QWGI3ozBZ/4mY6AABx9QEA
+rTRZ8U7JxlVC/iX8fMzPNC3Ynmh6cH9feo7IP/DBVmAA/AmpD4UbPIrViv9jCHIN
+EQUjXZv3x5ZYr8Fx4ic4ldgF
+=eVnv
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/playwright/apache-test-0.2/apache-test-0.2.tar.gz
b/playwright/apache-test-0.2/apache-test-0.2.tar.gz
new file mode 100644
index 0000000..cdf2aa6
Binary files /dev/null and b/playwright/apache-test-0.2/apache-test-0.2.tar.gz
differ
diff --git a/playwright/apache-test-0.2/apache-test-0.2.tar.gz.asc
b/playwright/apache-test-0.2/apache-test-0.2.tar.gz.asc
new file mode 100644
index 0000000..ee315f7
--- /dev/null
+++ b/playwright/apache-test-0.2/apache-test-0.2.tar.gz.asc
@@ -0,0 +1,8 @@
+-----BEGIN PGP SIGNATURE-----
+
+wr0EABYKAG8FgmjtT7AJEIGxoLjZ0gy6RxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
+LnNlcXVvaWEtcGdwLm9yZ4VkofDuJJfmgWXBNbtwfchEvmhkIDXiWM6KftpKJ6HY
+FiEEoy8PzP9IyU+5cfmFgbGguNnSDLoAAGw/AQDAk4F3V56S1Rtrirm/ACTN59wu
+L4OohdalGj+tXF45mwD+OssTPHA5li7U6oDv3Aj3zXtCD6zWqfumaW0ePCxzOQs=
+=QK4u
+-----END PGP SIGNATURE-----
diff --git a/playwright/apache-test-0.2/apache-test-0.2.tar.gz.sha512
b/playwright/apache-test-0.2/apache-test-0.2.tar.gz.sha512
new file mode 100644
index 0000000..bc07ae9
--- /dev/null
+++ b/playwright/apache-test-0.2/apache-test-0.2.tar.gz.sha512
@@ -0,0 +1 @@
+c6268245288e458795dbbc30b254493df29ce0829d770e0e22002cdb74043ef79e98436e7a1e67371974288cd13964a99538047ef53a4e4659075af283ee7c50
apache-test-0.2.tar.gz
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
deleted file mode 100644
index 0512b3c..0000000
Binary files
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz
and /dev/null differ
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
deleted file mode 100644
index 7d5b50b..0000000
---
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.asc
+++ /dev/null
@@ -1,8 +0,0 @@
------BEGIN PGP SIGNATURE-----
-
-wr0EABYKAG8FgmhayrkJEE9m+vmdYrj0RxQAAAAAAB4AIHNhbHRAbm90YXRpb25z
-LnNlcXVvaWEtcGdwLm9yZ3vpwgIRTsF5adxcqdebGCCmSGKvgOzuVzTFzEdfkCIW
-FiEEb6YmD+8n2tqPMh+RT2b6+Z1iuPQAAPZAAQC2wpFu44Z10oaQHvXAoWdHWH1r
-rh0ypJtdf8m8Z3fU7gD9EbDlFqqr1g3VE+52ftONNAaOOCNFIRUAcajGjphXjws=
-=8tSd
------END PGP SIGNATURE-----
diff --git
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
b/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
deleted file mode 100644
index 06bb2c0..0000000
---
a/playwright/apache-tooling-test-example-0.2/apache-tooling-test-example-0.2.tar.gz.sha512
+++ /dev/null
@@ -1 +0,0 @@
-82709c7c20145bff4a62728a305d93c1a07f3082e209b147dd174dc31ddd151f0164a37a62dddb68c20eb8e21404199cd1863ea946438aecf509ced57d583a8c
apache-tooling-test-example-0.2.tar.gz
diff --git a/playwright/mk.sh b/playwright/mk.sh
index 99d7ff4..8fbc956 100755
--- a/playwright/mk.sh
+++ b/playwright/mk.sh
@@ -28,16 +28,17 @@ mv tmp.secret.asc "${_fp}.secret.asc"
sq key delete --cert-file "${_fp}.secret.asc" --output "${_fp}.asc"
# Enter the directory containing the artifact
-cd apache-tooling-test-example-0.2/
+cd apache-test-0.2/
+rm ./*.asc ./*.sha512
# Generate the SHA-2-512 hash
-sha512sum apache-tooling-test-example-0.2.tar.gz > \
- apache-tooling-test-example-0.2.tar.gz.sha512
+sha512sum apache-test-0.2.tar.gz > \
+ apache-test-0.2.tar.gz.sha512
# Generate the signature
sq sign --signer-file "../${_fp}.secret.asc" \
- --signature-file apache-tooling-test-example-0.2.tar.gz > \
- apache-tooling-test-example-0.2.tar.gz.asc
+ --signature-file apache-test-0.2.tar.gz.asc \
+ apache-test-0.2.tar.gz
# Remove the secret key
rm "../${_fp}.secret.asc"
diff --git a/playwright/test.py b/playwright/test.py
index 80e8b54..5c8ff11 100755
--- a/playwright/test.py
+++ b/playwright/test.py
@@ -19,7 +19,6 @@
import argparse
import dataclasses
-import getpass
import glob
import logging
import os
@@ -35,9 +34,10 @@ import netifaces
import playwright.sync_api as sync_api
import rich.logging
-_SSH_KEY_COMMENT: Final[str] = "[email protected]"
-_SSH_KEY_PATH: Final[str] = "/root/.ssh/id_ed25519"
-_OPENPGP_TEST_UID: Final[str] = "<[email protected]>"
+OPENPGP_TEST_UID: Final[str] = "<[email protected]>"
+SSH_KEY_COMMENT: Final[str] = "[email protected]"
+SSH_KEY_PATH: Final[str] = "/root/.ssh/id_ed25519"
+TEST_PROJECT: Final[str] = "test"
@dataclasses.dataclass
@@ -58,19 +58,23 @@ def esc_id(text: str) -> str:
def get_credentials() -> Credentials | None:
- try:
- username = input("Enter ASF Username: ")
- password = getpass.getpass("Enter ASF Password: ")
- except (EOFError, KeyboardInterrupt):
- print()
- logging.error("EOFError: No credentials provided")
- return None
+ return Credentials(username="test", password="test")
- if (not username) or (not password):
- logging.error("Username and password cannot be empty")
- return None
- return Credentials(username=username, password=password)
+# def get_credentials_custom() -> Credentials | None:
+# try:
+# username = input("Enter ASF Username: ")
+# password = getpass.getpass("Enter ASF Password: ")
+# except (EOFError, KeyboardInterrupt):
+# print()
+# logging.error("EOFError: No credentials provided")
+# return None
+#
+# if (not username) or (not password):
+# logging.error("Username and password cannot be empty")
+# return None
+#
+# return Credentials(username=username, password=password)
def get_default_gateway_ip() -> str | None:
@@ -95,10 +99,12 @@ def go_to_path(page: sync_api.Page, path: str, wait: bool =
True) -> None:
def lifecycle_01_add_draft(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
logging.info("Following link to start a new release")
- go_to_path(page, "/start/tooling-test-example")
+ go_to_path(page, f"/start/{TEST_PROJECT}")
logging.info("Waiting for the start new release page")
version_name_locator = page.locator("input#version_name")
+ if not version_name_locator.is_visible(timeout=1000):
+ logging.error(f"Version name input not found. Page
content:\n{page.content()}")
sync_api.expect(version_name_locator).to_be_visible()
logging.info("Start new release page loaded")
@@ -110,24 +116,24 @@ def lifecycle_01_add_draft(page: sync_api.Page,
credentials: Credentials, versio
sync_api.expect(submit_button_locator).to_be_enabled()
submit_button_locator.click()
- logging.info(f"Waiting for navigation to
/compose/tooling-test-example/{version_name} after adding draft")
- wait_for_path(page, f"/compose/tooling-test-example/{version_name}")
+ logging.info(f"Waiting for navigation to
/compose/{TEST_PROJECT}/{version_name} after adding draft")
+ wait_for_path(page, f"/compose/{TEST_PROJECT}/{version_name}")
logging.info("Add draft actions completed successfully")
def lifecycle_02_check_draft_added(page: sync_api.Page, credentials:
Credentials, version_name: str) -> None:
- logging.info(f"Checking for draft 'tooling-test-example {version_name}'")
- go_to_path(page, f"/compose/tooling-test-example/{version_name}")
- h1_strong_locator = page.locator("h1 strong:has-text('Tooling Test
Example')")
+ logging.info(f"Checking for draft '{TEST_PROJECT} {version_name}'")
+ go_to_path(page, f"/compose/{TEST_PROJECT}/{version_name}")
+ h1_strong_locator = page.locator("h1 strong:has-text('Test')")
sync_api.expect(h1_strong_locator).to_be_visible()
h1_em_locator = page.locator(f"h1 em:has-text('{esc_id(version_name)}')")
sync_api.expect(h1_em_locator).to_be_visible()
- logging.info(f"Draft 'tooling-test-example {version_name}' found
successfully")
+ logging.info(f"Draft '{TEST_PROJECT} {version_name}' found successfully")
def lifecycle_03_add_file(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
- logging.info(f"Navigating to the upload file page for tooling-test-example
{version_name}")
- go_to_path(page, f"/upload/tooling-test-example/{version_name}")
+ logging.info(f"Navigating to the upload file page for {TEST_PROJECT}
{version_name}")
+ go_to_path(page, f"/upload/{TEST_PROJECT}/{version_name}")
logging.info("Upload file page loaded")
logging.info("Locating the file input")
@@ -142,21 +148,21 @@ def lifecycle_03_add_file(page: sync_api.Page,
credentials: Credentials, version
sync_api.expect(submit_button_locator).to_be_enabled()
submit_button_locator.click()
- logging.info(f"Waiting for navigation to
/compose/tooling-test-example/{version_name} after adding file")
- wait_for_path(page, f"/compose/tooling-test-example/{version_name}")
+ logging.info(f"Waiting for navigation to
/compose/{TEST_PROJECT}/{version_name} after adding file")
+ wait_for_path(page, f"/compose/{TEST_PROJECT}/{version_name}")
logging.info("Add file actions completed successfully")
- logging.info("Navigating back to /compose/tooling-test-example")
- go_to_path(page, f"/compose/tooling-test-example/{version_name}")
- logging.info("Navigation back to /compose/tooling-test-example completed
successfully")
+ logging.info(f"Navigating back to /compose/{TEST_PROJECT}/{version_name}")
+ go_to_path(page, f"/compose/{TEST_PROJECT}/{version_name}")
+ logging.info(f"Navigation back to /compose/{TEST_PROJECT}/{version_name}
completed successfully")
def lifecycle_04_start_vote(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
- logging.info(f"Navigating to the compose/tooling-test-example page for
tooling-test-example {version_name}")
- go_to_path(page, f"/compose/tooling-test-example/{version_name}")
- logging.info("Compose/tooling-test-example page loaded successfully")
+ logging.info(f"Navigating to the compose/{TEST_PROJECT} page for
{TEST_PROJECT} {version_name}")
+ go_to_path(page, f"/compose/{TEST_PROJECT}/{version_name}")
+ logging.info(f"Compose/{TEST_PROJECT} page loaded successfully")
- logging.info(f"Locating start vote link for tooling-test-example
{version_name}")
+ logging.info(f"Locating start vote link for {TEST_PROJECT} {version_name}")
start_vote_link_locator = page.locator('a[title="Start a vote on this
draft"]')
sync_api.expect(start_vote_link_locator).to_be_visible()
@@ -173,15 +179,15 @@ def lifecycle_04_start_vote(page: sync_api.Page,
credentials: Credentials, versi
sync_api.expect(submit_button_locator).to_be_enabled()
submit_button_locator.click()
- logging.info(f"Waiting for navigation to
/vote/tooling-test-example/{version_name} after submitting vote email")
- wait_for_path(page, f"/vote/tooling-test-example/{version_name}")
+ logging.info(f"Waiting for navigation to
/vote/{TEST_PROJECT}/{version_name} after submitting vote email")
+ wait_for_path(page, f"/vote/{TEST_PROJECT}/{version_name}")
logging.info("Vote initiation actions completed successfully")
def lifecycle_05_resolve_vote(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
- logging.info(f"Navigating to the vote page for tooling-test-example
{version_name}")
- go_to_path(page, f"/vote/tooling-test-example/{version_name}")
+ logging.info(f"Navigating to the vote page for {TEST_PROJECT}
{version_name}")
+ go_to_path(page, f"/vote/{TEST_PROJECT}/{version_name}")
logging.info("Vote page loaded successfully")
# Wait until the vote initiation background task has completed
@@ -202,7 +208,7 @@ def lifecycle_05_resolve_vote(page: sync_api.Page,
credentials: Credentials, ver
logging.warning("Vote initiation banner not detected after 15s,
proceeding anyway")
logging.info("Locating the 'Resolve vote' button")
- tabulate_form_locator =
page.locator(f'form[action="/resolve/tabulated/tooling-test-example/{version_name}"]')
+ tabulate_form_locator =
page.locator(f'form[action="/resolve/tabulated/{TEST_PROJECT}/{version_name}"]')
sync_api.expect(tabulate_form_locator).to_be_visible()
tabulate_button_locator =
tabulate_form_locator.locator('button[type="submit"]:has-text("Resolve vote")')
@@ -211,10 +217,10 @@ def lifecycle_05_resolve_vote(page: sync_api.Page,
credentials: Credentials, ver
tabulate_button_locator.click()
logging.info("Waiting for navigation to tabulated votes page")
- wait_for_path(page,
f"/resolve/tabulated/tooling-test-example/{version_name}")
+ wait_for_path(page, f"/resolve/tabulated/{TEST_PROJECT}/{version_name}")
logging.info("Locating the resolve vote form on the tabulated votes page")
- resolve_form_locator =
page.locator(f'form[action="/resolve/submit/tooling-test-example/{version_name}"]')
+ resolve_form_locator =
page.locator(f'form[action="/resolve/submit/{TEST_PROJECT}/{version_name}"]')
sync_api.expect(resolve_form_locator).to_be_visible()
logging.info("Selecting 'Passed' radio button in resolve form")
@@ -227,25 +233,25 @@ def lifecycle_05_resolve_vote(page: sync_api.Page,
credentials: Credentials, ver
sync_api.expect(resolve_submit_locator).to_be_enabled()
resolve_submit_locator.click()
- logging.info(f"Waiting for navigation to
/finish/tooling-test-example/{version_name} after resolving the vote")
- wait_for_path(page, f"/finish/tooling-test-example/{version_name}")
+ logging.info(f"Waiting for navigation to
/finish/{TEST_PROJECT}/{version_name} after resolving the vote")
+ wait_for_path(page, f"/finish/{TEST_PROJECT}/{version_name}")
logging.info("Vote resolution actions completed successfully")
def lifecycle_06_announce_preview(page: sync_api.Page, credentials:
Credentials, version_name: str) -> None:
- go_to_path(page, f"/finish/tooling-test-example/{version_name}")
+ go_to_path(page, f"/finish/{TEST_PROJECT}/{version_name}")
logging.info("Finish page loaded successfully")
- logging.info(f"Locating the announce link for tooling-test-example
{version_name}")
- announce_link_locator =
page.locator(f'a[href="/announce/tooling-test-example/{esc_id(version_name)}"]')
+ logging.info(f"Locating the announce link for {TEST_PROJECT}
{version_name}")
+ announce_link_locator =
page.locator(f'a[href="/announce/{TEST_PROJECT}/{esc_id(version_name)}"]')
sync_api.expect(announce_link_locator).to_be_visible()
announce_link_locator.click()
- logging.info(f"Waiting for navigation to
/announce/tooling-test-example/{version_name} after announcing preview")
- wait_for_path(page, f"/announce/tooling-test-example/{version_name}")
+ logging.info(f"Waiting for navigation to
/announce/{TEST_PROJECT}/{version_name} after announcing preview")
+ wait_for_path(page, f"/announce/{TEST_PROJECT}/{version_name}")
- logging.info(f"Locating the announcement form for tooling-test-example
{version_name}")
- form_locator =
page.locator(f'form[action="/announce/tooling-test-example/{esc_id(version_name)}"]')
+ logging.info(f"Locating the announcement form for {TEST_PROJECT}
{version_name}")
+ form_locator =
page.locator(f'form[action="/announce/{TEST_PROJECT}/{esc_id(version_name)}"]')
sync_api.expect(form_locator).to_be_visible()
logging.info("Locating the confirmation checkbox within the form")
@@ -261,21 +267,19 @@ def lifecycle_06_announce_preview(page: sync_api.Page,
credentials: Credentials,
submit_button_locator.click()
logging.info("Waiting for navigation to /releases after submitting
announcement")
- wait_for_path(page, "/releases/finished/tooling-test-example")
+ wait_for_path(page, f"/releases/finished/{TEST_PROJECT}")
logging.info("Preview announcement actions completed successfully")
def lifecycle_07_release_exists(page: sync_api.Page, credentials: Credentials,
version_name: str) -> None:
- logging.info(f"Checking for release tooling-test-example {version_name} on
/releases/finished/tooling-test-example")
- go_to_path(page, "/releases/finished/tooling-test-example")
+ logging.info(f"Checking for release {TEST_PROJECT} {version_name} on
/releases/finished/{TEST_PROJECT}")
+ go_to_path(page, f"/releases/finished/{TEST_PROJECT}")
logging.info("Releases finished page loaded successfully")
release_card_locator =
page.locator(f'div.card:has(strong.card-title:has-text("{version_name}"))')
sync_api.expect(release_card_locator).to_be_visible()
- logging.info(f"Found card for tooling-test-example {version_name} release")
- logging.info(
- f"Release tooling-test-example {version_name} confirmed exists on
/releases/finished/tooling-test-example"
- )
+ logging.info(f"Found card for {TEST_PROJECT} {version_name} release")
+ logging.info(f"Release {TEST_PROJECT} {version_name} confirmed exists on
/releases/finished/{TEST_PROJECT}")
def main() -> None:
@@ -464,7 +468,7 @@ def slow(func: Callable[..., Any]) -> Callable[..., Any]:
def ssh_keys_generate() -> None:
- ssh_key_path = _SSH_KEY_PATH
+ ssh_key_path = SSH_KEY_PATH
ssh_dir = os.path.dirname(ssh_key_path)
try:
@@ -477,9 +481,9 @@ def ssh_keys_generate() -> None:
os.makedirs(ssh_dir, mode=0o700, exist_ok=True)
- logging.info(f"Generating new SSH key at {ssh_key_path} with comment
{_SSH_KEY_COMMENT}")
+ logging.info(f"Generating new SSH key at {ssh_key_path} with comment
{SSH_KEY_COMMENT}")
subprocess.run(
- ["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path, "-N", "",
"-C", _SSH_KEY_COMMENT],
+ ["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path, "-N", "",
"-C", SSH_KEY_COMMENT],
check=True,
capture_output=True,
text=True,
@@ -504,7 +508,6 @@ def test_all(page: sync_api.Page, credentials: Credentials,
skip_slow: bool) ->
tests["projects"] = [
test_projects_01_update,
test_projects_02_check_directory,
- test_projects_03_add_project,
]
tests["lifecycle"] = [
test_lifecycle_01_add_draft,
@@ -542,7 +545,7 @@ def test_all(page: sync_api.Page, credentials: Credentials,
skip_slow: bool) ->
def test_checks_01_hashing_sha512(page: sync_api.Page, credentials:
Credentials) -> None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_sha512 = f"apache-{project_name}-{version_name}.tar.gz.sha512"
compose_path = f"/compose/{project_name}/{version_name}"
@@ -579,7 +582,7 @@ def test_checks_01_hashing_sha512(page: sync_api.Page,
credentials: Credentials)
def test_checks_02_license_files(page: sync_api.Page, credentials:
Credentials) -> None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
compose_path = f"/compose/{project_name}/{version_name}"
@@ -616,7 +619,7 @@ def test_checks_02_license_files(page: sync_api.Page,
credentials: Credentials)
def test_checks_03_license_headers(page: sync_api.Page, credentials:
Credentials) -> None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
report_file_path =
f"/report/{project_name}/{version_name}/{filename_targz}"
@@ -641,7 +644,7 @@ def test_checks_03_license_headers(page: sync_api.Page,
credentials: Credentials
def test_checks_04_paths(page: sync_api.Page, credentials: Credentials) ->
None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_sha512 = f"apache-{project_name}-{version_name}.tar.gz.sha512"
report_file_path =
f"/report/{project_name}/{version_name}/{filename_sha512}"
@@ -667,7 +670,7 @@ def test_checks_04_paths(page: sync_api.Page, credentials:
Credentials) -> None:
def test_checks_05_signature(page: sync_api.Page, credentials: Credentials) ->
None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_asc = f"apache-{project_name}-{version_name}.tar.gz.asc"
report_file_path = f"/report/{project_name}/{version_name}/{filename_asc}"
@@ -691,7 +694,7 @@ def test_checks_05_signature(page: sync_api.Page,
credentials: Credentials) -> N
def test_checks_06_targz(page: sync_api.Page, credentials: Credentials) ->
None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
filename_targz = f"apache-{project_name}-{version_name}.tar.gz"
report_file_path =
f"/report/{project_name}/{version_name}/{filename_targz}"
@@ -831,6 +834,12 @@ def test_login(page: sync_api.Page, credentials:
Credentials) -> None:
if debugging:
remove_debugging = test_logging_debug(page, credentials)
+ if credentials.username == "test":
+ go_to_path(page, "/test-login", wait=False)
+ wait_for_path(page, "/")
+ logging.info("Test login completed successfully")
+ return
+
go_to_path(page, "/")
logging.info(f"Initial page title: {page.title()}")
@@ -973,7 +982,7 @@ def test_ssh_01_add_key(page: sync_api.Page, credentials:
Credentials) -> None:
wait_for_path(page, "/keys/ssh/add")
logging.info("Navigated to Add your SSH key page")
- public_key_path = f"{_SSH_KEY_PATH}.pub"
+ public_key_path = f"{SSH_KEY_PATH}.pub"
try:
logging.info(f"Reading public key from {public_key_path}")
with open(public_key_path, encoding="utf-8") as f:
@@ -1022,7 +1031,7 @@ def test_ssh_01_add_key(page: sync_api.Page, credentials:
Credentials) -> None:
def test_ssh_02_rsync_upload(page: sync_api.Page, credentials: Credentials) ->
None:
- project_name = "tooling-test-example"
+ project_name = TEST_PROJECT
version_name = "0.2"
source_dir_rel = f"apache-{project_name}-{version_name}"
source_dir_abs = f"/run/tests/{source_dir_rel}"
@@ -1093,16 +1102,29 @@ def test_ssh_02_rsync_upload(page: sync_api.Page,
credentials: Credentials) -> N
def test_tidy_up(page: sync_api.Page) -> None:
- # Projects cannot be deleted if they have associated releases
- # Therefore, we need to delete releases first
test_tidy_up_releases(page)
- test_tidy_up_project(page)
test_tidy_up_ssh_keys(page)
test_tidy_up_openpgp_keys(page)
def test_tidy_up_openpgp_keys(page: sync_api.Page) -> None:
logging.info("Starting OpenPGP key tidy up")
+
+ # First, delete the test key if it exists with wrong apache_uid
+ # (it may exist from real usage due to on_conflict_do_nothing in the
INSERT)
+ # TODO: Don't hardcode this
+ logging.info("Deleting test key from database via admin route")
+
+ # Navigate to the delete route and submit the form
+ go_to_path(page, "/admin/delete-test-openpgp-keys")
+ delete_button = page.locator('button[type="submit"]')
+ if delete_button.is_visible():
+ delete_button.click()
+ page.wait_for_load_state()
+ logging.info("Test key deletion form submitted")
+ else:
+ logging.info("Test key deletion button not found, key may not exist")
+
go_to_path(page, "/keys")
logging.info("Navigated to /keys page for OpenPGP key cleanup")
@@ -1129,7 +1151,7 @@ def test_tidy_up_openpgp_keys(page: sync_api.Page) ->
None:
sync_api.expect(pre_locator).to_be_visible()
key_content = pre_locator.inner_text()
- if _OPENPGP_TEST_UID in key_content:
+ if OPENPGP_TEST_UID in key_content:
logging.info(f"Found test OpenPGP key with fingerprint
{fingerprint} for deletion")
fingerprints_to_delete.append(fingerprint)
@@ -1179,7 +1201,7 @@ def test_tidy_up_openpgp_keys_continued(page:
sync_api.Page, fingerprints_to_del
def test_tidy_up_project(page: sync_api.Page) -> None:
- project_name = "Apache Tooling Test Example"
+ project_name = "Apache Test"
logging.info(f"Checking for project '{project_name}' at /projects")
go_to_path(page, "/projects")
logging.info("Project directory page loaded")
@@ -1268,7 +1290,7 @@ def test_tidy_up_ssh_keys(page: sync_api.Page) -> None:
continue
key_content = details_pre_locator.inner_text()
- if _SSH_KEY_COMMENT in key_content:
+ if SSH_KEY_COMMENT in key_content:
fingerprint_td_locator = card.locator('td:has-text("SHA256:")')
if fingerprint_td_locator.is_visible(timeout=500):
fingerprint = fingerprint_td_locator.inner_text().strip()
@@ -1280,7 +1302,7 @@ def test_tidy_up_ssh_keys(page: sync_api.Page) -> None:
else:
logging.warning("Could not locate fingerprint td for a test
key card")
else:
- logging.debug(f"SSH key card: test comment '{_SSH_KEY_COMMENT}'
not found in key content")
+ logging.debug(f"SSH key card: test comment '{SSH_KEY_COMMENT}' not
found in key content")
# For the complexity linter only
test_tidy_up_ssh_keys_continued(page, fingerprints_to_delete)
@@ -1331,11 +1353,11 @@ def test_tidy_up_releases(page: sync_api.Page) -> None:
logging.info("Admin delete release page loaded")
# TODO: Get these names automatically
- release_remove(page, "tooling-test-example-0.1+draft")
- release_remove(page, "tooling-test-example-0.1+candidate")
- release_remove(page, "tooling-test-example-0.1+preview")
- release_remove(page, "tooling-test-example-0.1+release")
- release_remove(page, "tooling-test-example-0.2")
+ release_remove(page, f"{TEST_PROJECT}-0.1+draft")
+ release_remove(page, f"{TEST_PROJECT}-0.1+candidate")
+ release_remove(page, f"{TEST_PROJECT}-0.1+preview")
+ release_remove(page, f"{TEST_PROJECT}-0.1+release")
+ release_remove(page, f"{TEST_PROJECT}-0.2")
def wait_for_path(page: sync_api.Page, path: str) -> None:
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]