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
commit ab2467efc9689d2cbce23f83b001ac75664892ba Author: Sean B. Palmer <[email protected]> AuthorDate: Tue Nov 11 19:36:54 2025 +0000 Make some key deletion and update forms more type safe --- atr/get/keys.py | 214 +++++++++++++++++++++++++++++++++----- atr/htm.py | 15 +++ atr/post/keys.py | 93 ++++++++++------- atr/shared/keys.py | 26 ++++- atr/templates/committee-view.html | 4 +- atr/templates/keys-review.html | 161 ---------------------------- playwright/test.py | 13 +-- 7 files changed, 287 insertions(+), 239 deletions(-) diff --git a/atr/get/keys.py b/atr/get/keys.py index a9eb88d..f69c46f 100644 --- a/atr/get/keys.py +++ b/atr/get/keys.py @@ -15,14 +15,14 @@ # specific language governing permissions and limitations # under the License. -import datetime -import quart +import markupsafe import atr.blueprints.get as get import atr.db as db import atr.form as form import atr.htm as htm +import atr.models.sql as sql import atr.post as post import atr.shared as shared import atr.storage as storage @@ -40,9 +40,9 @@ async def add(session: web.Committer) -> str: committee_choices = [(c.name, c.display_name or c.name) for c in participant_of_committees] page = htm.Block() - page.p[htm.a(href=util.as_url(keys), class_="atr-back-link")["← Back to Manage keys"],] - page.div(class_="my-4")[ - htm.h1(class_="mb-4")["Add your OpenPGP key"], + page.p[htm.a(".atr-back-link", href=util.as_url(keys))["← Back to Manage keys"],] + page.div(".my-4")[ + htm.h1(".mb-4")["Add your OpenPGP key"], htm.p["Add your public key to use for signing release artifacts."], ] form.render_block( @@ -56,6 +56,35 @@ async def add(session: web.Committer) -> str: }, ) + page.append( + htm.script[ + markupsafe.Markup(""" + document.addEventListener("DOMContentLoaded", function() { + const checkboxes = document.querySelectorAll("input[name='selected_committees']"); + if (checkboxes.length === 0) return; + + const firstCheckbox = checkboxes[0]; + const container = firstCheckbox.closest(".col-sm-8"); + if (!container) return; + + const button = document.createElement("button"); + button.id = "toggleCommitteesBtn"; + button.type = "button"; + button.className = "btn btn-outline-secondary btn-sm mt-2"; + button.textContent = "Select all committees"; + + button.addEventListener("click", function() { + const allChecked = Array.from(checkboxes).every(cb => cb.checked); + checkboxes.forEach(cb => cb.checked = !allChecked); + button.textContent = allChecked ? "Select all committees" : "Deselect all committees"; + }); + + container.appendChild(button); + }); + """) + ] + ) + return await template.blank( "Add your OpenPGP key", content=page.collect(), @@ -84,9 +113,6 @@ async def keys(session: web.Committer) -> str: """View all keys associated with the user's account.""" committees_to_query = list(set(session.committees + session.projects)) - delete_form = await shared.keys.DeleteKeyForm.create_form() - update_committee_keys_form = await shared.keys.UpdateCommitteeKeysForm.create_form() - async with db.session() as data: user_keys = await data.public_signing_key(apache_uid=session.uid.lower(), _committees=True).all() user_ssh_keys = await data.ssh_key(asf_uid=session.uid).all() @@ -94,23 +120,28 @@ async def keys(session: web.Committer) -> str: for key in user_keys: key.committees.sort(key=lambda c: c.name) - status_message = quart.request.args.get("status_message") - status_type = quart.request.args.get("status_type") - - return await template.render( - "keys-review.html", - asf_id=session.uid, - user_keys=user_keys, - user_ssh_keys=user_ssh_keys, - committees=user_committees_with_keys, - algorithms=shared.algorithms, - status_message=status_message, - status_type=status_type, - now=datetime.datetime.now(datetime.UTC), - delete_form=delete_form, - update_committee_keys_form=update_committee_keys_form, - email_from_key=util.email_from_uid, - committee_is_standing=util.committee_is_standing, + page = htm.Block() + page.h1["Manage keys"] + page.p(".mb-4")[ + htm.a(".btn.btn-sm.btn-secondary.me-3", href="#your-public-keys")["Your public keys"], + htm.a(".btn.btn-sm.btn-secondary", href="#your-committee-keys")["Your committee's keys"], + ] + + page.h2("#your-public-keys")["Your public keys"] + page.p["Review your public keys used for signing release artifacts."] + page.div(".d-flex.gap-3.mb-4")[ + htm.a(".btn.btn-outline-primary", href=util.as_url(add))["Add your OpenPGP key"], + htm.a(".btn.btn-outline-primary", href=util.as_url(ssh_add))["Add your SSH key"], + ] + + _openpgp_keys(page, list(user_keys)) + _ssh_keys(page, list(user_ssh_keys)) + _committee_keys(page, list(user_committees_with_keys)) + + return await template.blank( + "Manage keys", + content=page.collect(), + description="Review your keys.", ) @@ -124,3 +155,136 @@ async def ssh_add(session: web.Committer) -> web.WerkzeugResponse | str: async def upload(session: web.Committer) -> str: """Upload a KEYS file containing multiple OpenPGP keys.""" return await shared.keys.upload(session) + + +def _committee_keys(page: htm.Block, user_committees_with_keys: list[sql.Committee]) -> None: + page.h2("#your-committee-keys")["Your committee's keys"] + page.div(".mb-4")[htm.a(".btn.btn-outline-primary", href=util.as_url(upload))["Upload a KEYS file"]] + + for committee in user_committees_with_keys: + if not util.committee_is_standing(committee.name): + page.h3(f"#committee-{committee.name}.mt-3")[committee.display_name or committee.name] + + if committee.public_signing_keys: + thead = htm.thead[ + htm.tr[ + htm.th(".px-2", scope="col")["Key ID"], + htm.th(".px-2", scope="col")["Email"], + htm.th(".px-2", scope="col")["Apache UID"], + ] + ] + tbody = htm.Block(htm.tbody) + for key in committee.public_signing_keys: + row = htm.Block(htm.tr) + details_url = util.as_url(details, fingerprint=key.fingerprint) + row.td(".text-break.font-monospace.px-2")[htm.a(href=details_url)[key.fingerprint[-16:].upper()]] + email = util.email_from_uid(key.primary_declared_uid) if key.primary_declared_uid else "-" + row.td(".text-break.px-2")[email or "-"] + row.td(".text-break.px-2")[key.apache_uid or "-"] + tbody.append(row.collect()) + + page.div(".table-responsive.mb-2")[ + htm.table(".table.border.table-striped.table-hover.table-sm")[thead, tbody.collect()] + ] + page.p(".text-muted")[ + "The ", + htm.code["KEYS"], + " file is automatically generated when you add or remove a key," + " but you can also use the form below to manually regenerate it.", + ] + + form.render_block( + page, + model_cls=shared.keys.UpdateCommitteeKeysForm, + action=util.as_url(post.keys.keys), + form_classes=".mb-4.d-inline-block", + submit_label="Regenerate KEYS file", + submit_classes="btn btn-sm btn-outline-secondary", + defaults={"committee_name": committee.name}, + empty=True, + ) + else: + page.p(".mb-4")["No keys uploaded for this committee yet."] + + +def _openpgp_keys(page: htm.Block, user_keys: list[sql.PublicSigningKey]) -> None: + page.h3["Your OpenPGP keys"] + if user_keys: + thead = htm.thead[ + htm.tr[ + htm.th(".px-2", scope="col")["Key ID"], + htm.th(".px-2", scope="col")["Committees"], + htm.th(".px-2", scope="col")["Action"], + ] + ] + + tbody = htm.Block(htm.tbody) + for key in user_keys: + row = htm.Block(htm.tr, classes=".page-user-openpgp-key") + row.td(".text-break.px-2.align-middle")[ + htm.a(href=util.as_url(details, fingerprint=key.fingerprint))[key.fingerprint[-16:].upper()] + ] + if key.committees: + committee_names = ", ".join([c.name for c in key.committees]) + row.td(".text-break.px-2.align-middle")[committee_names] + else: + row.td(".text-break.px-2.align-middle")["No PMCs associated"] + with row.block(htm.td, classes=".px-2") as td: + form.render_block( + td, + model_cls=shared.keys.DeleteOpenPGPKeyForm, + action=util.as_url(post.keys.keys), + form_classes=".m-0", + submit_label="Delete key", + submit_classes="btn btn-sm btn-danger", + defaults={"fingerprint": key.fingerprint}, + empty=True, + ) + tbody.append(row.collect()) + + page.div(".table-responsive.mb-5")[ + htm.table(".table.border.table-striped.table-hover.table-sm")[thead, tbody.collect()] + ] + else: + page.p[htm.strong["You haven't added any personal OpenPGP keys yet."]] + + +def _ssh_keys(page: htm.Block, user_ssh_keys: list[sql.SSHKey]) -> None: + page.h3["Your SSH keys"] + if user_ssh_keys: + grid = htm.Block(htm.div, classes=".d-grid.gap-4") + for key in user_ssh_keys: + card_block = htm.Block(htm.div, classes=f"#ssh-key-{key.fingerprint}.card.p-3.border") + + key_type = key.key.split()[0] if key.key else "" + tbody = htm.tbody[ + htm.tr[ + htm.th(".p-2.text-dark")["Fingerprint"], + htm.td(".text-break")[key.fingerprint], + ], + htm.tr[ + htm.th(".p-2.text-dark")["Type"], + htm.td(".text-break")[key_type], + ], + ] + card_block.table(".mb-0")[tbody] + card_block.details(".mt-3.p-3.bg-light.rounded")[ + htm.summary(".fw-bold")["View whole key"], + htm.pre(".mt-3")[key.key], + ] + + form.render_block( + card_block, + model_cls=shared.keys.DeleteSSHKeyForm, + action=util.as_url(post.keys.keys), + form_classes=".mt-3", + submit_label="Delete key", + submit_classes="btn btn-danger", + defaults={"fingerprint": key.fingerprint}, + empty=True, + ) + grid.append(card_block.collect()) + + page.div(".mb-5.p-4.bg-light.rounded")[grid.collect()] + else: + page.p[htm.strong["You haven't added any SSH keys yet."]] diff --git a/atr/htm.py b/atr/htm.py index 240ccaf..3810099 100644 --- a/atr/htm.py +++ b/atr/htm.py @@ -242,9 +242,24 @@ class Block: self.__check_parent("table", {"body", "div"}) return BlockElementCallable(self, table) + @property + def td(self) -> BlockElementCallable: + self.__check_parent("td", {"tr"}) + return BlockElementCallable(self, td) + def text(self, text: str) -> None: self.elements.append(text) + @property + def th(self) -> BlockElementCallable: + self.__check_parent("th", {"tr"}) + return BlockElementCallable(self, th) + + @property + def thead(self) -> BlockElementCallable: + self.__check_parent("thead", {"table"}) + return BlockElementCallable(self, thead) + @property def title(self) -> BlockElementCallable: self.__check_parent("title", {"head", "html"}) diff --git a/atr/post/keys.py b/atr/post/keys.py index 36054cb..28b221f 100644 --- a/atr/post/keys.py +++ b/atr/post/keys.py @@ -73,36 +73,69 @@ async def add(session: web.Committer, add_openpgp_key_form: shared.keys.AddOpenP return await session.redirect(get.keys.keys) [email protected]("/keys/delete") -async def delete(session: web.Committer) -> web.WerkzeugResponse: - """Delete a public signing key or SSH key from the user's account.""" - form = await shared.keys.DeleteKeyForm.create_form(data=await quart.request.form) [email protected]("/keys") [email protected](shared.keys.KeysForm) +async def keys(session: web.Committer, keys_form: shared.keys.KeysForm) -> web.WerkzeugResponse: + """Handle forms on the keys management page.""" + match keys_form: + case shared.keys.DeleteOpenPGPKeyForm() as delete_openpgp_form: + return await _delete_openpgp_key(session, delete_openpgp_form) - if not await form.validate_on_submit(): - return await session.redirect(get.keys.keys, error="Invalid request for key deletion.") + case shared.keys.DeleteSSHKeyForm() as delete_ssh_form: + return await _delete_ssh_key(session, delete_ssh_form) - fingerprint = (await quart.request.form).get("fingerprint") - if not fingerprint: - return await session.redirect(get.keys.keys, error="Missing key fingerprint for deletion.") + case shared.keys.UpdateCommitteeKeysForm() as update_committee_form: + return await _update_committee_keys(session, update_committee_form) + + +async def _delete_openpgp_key( + session: web.Committer, delete_form: shared.keys.DeleteOpenPGPKeyForm +) -> web.WerkzeugResponse: + """Delete an OpenPGP key from the user's account.""" + fingerprint = delete_form.fingerprint - # Try to delete an SSH key first - # Otherwise, delete an OpenPGP key - # TODO: Unmerge this, or identify the key type async with storage.write() as write: wafc = write.as_foundation_committer() - try: - await wafc.ssh.delete_key(fingerprint) - except storage.AccessError: - pass - else: - return await session.redirect(get.keys.keys, success="SSH key deleted successfully") oc: outcome.Outcome[sql.PublicSigningKey] = await wafc.keys.delete_key(fingerprint) match oc: case outcome.Result(): - return await session.redirect(get.keys.keys, success="Key deleted successfully") + return await session.redirect(get.keys.keys, success="OpenPGP key deleted successfully") case outcome.Error(error): - return await session.redirect(get.keys.keys, error=f"Error deleting key: {error}") + return await session.redirect(get.keys.keys, error=f"Error deleting OpenPGP key: {error}") + + +async def _delete_ssh_key(session: web.Committer, delete_form: shared.keys.DeleteSSHKeyForm) -> web.WerkzeugResponse: + """Delete an SSH key from the user's account.""" + fingerprint = delete_form.fingerprint + + async with storage.write() as write: + wafc = write.as_foundation_committer() + try: + await wafc.ssh.delete_key(fingerprint) + except storage.AccessError as e: + return await session.redirect(get.keys.keys, error=f"Error deleting SSH key: {e}") + + return await session.redirect(get.keys.keys, success="SSH key deleted successfully") + + +async def _update_committee_keys( + session: web.Committer, update_form: shared.keys.UpdateCommitteeKeysForm +) -> web.WerkzeugResponse: + """Regenerate the KEYS file for a committee.""" + committee_name = update_form.committee_name + + async with storage.write() as write: + wacm = write.as_committee_member(committee_name) + match await wacm.keys.autogenerate_keys_file(): + case outcome.Result(): + await quart.flash( + f'Successfully regenerated the KEYS file for the "{committee_name}" committee.', "success" + ) + case outcome.Error(): + await quart.flash(f"Error regenerating the KEYS file for the {committee_name} committee.", "error") + + return await session.redirect(get.keys.keys) @post.committer("/keys/details/<fingerprint>") @@ -138,26 +171,6 @@ async def ssh_add(session: web.Committer) -> web.WerkzeugResponse | str: return await shared.keys.ssh_add(session) [email protected]("/keys/update-committee-keys/<committee_name>") -async def update_committee_keys(session: web.Committer, committee_name: str) -> web.WerkzeugResponse: - """Generate and save the KEYS file for a specific committee.""" - form = await shared.keys.UpdateCommitteeKeysForm.create_form() - if not await form.validate_on_submit(): - return await session.redirect(get.keys.keys, error="Invalid request to update KEYS file.") - - async with storage.write() as write: - wacm = write.as_committee_member(committee_name) - match await wacm.keys.autogenerate_keys_file(): - case outcome.Result(): - await quart.flash( - f'Successfully regenerated the KEYS file for the "{committee_name}" committee.', "success" - ) - case outcome.Error(): - await quart.flash(f"Error regenerating the KEYS file for the {committee_name} committee.", "error") - - return await session.redirect(get.keys.keys) - - @post.committer("/keys/upload") async def upload(session: web.Committer) -> str: """Upload a KEYS file containing multiple OpenPGP keys.""" diff --git a/atr/shared/keys.py b/atr/shared/keys.py index 8da536f..4ec8484 100644 --- a/atr/shared/keys.py +++ b/atr/shared/keys.py @@ -20,6 +20,7 @@ import asyncio import datetime from collections.abc import Awaitable, Callable, Sequence +from typing import Annotated, Literal import aiohttp import asfquart.base as base @@ -42,6 +43,10 @@ import atr.user as user import atr.util as util import atr.web as web +type DELETE_OPENPGP_KEY = Literal["delete_openpgp_key"] +type DELETE_SSH_KEY = Literal["delete_ssh_key"] +type UPDATE_COMMITTEE_KEYS = Literal["update_committee_keys"] + class AddOpenPGPKeyForm(form.Form): public_key: str = form.label( @@ -74,12 +79,25 @@ class AddSSHKeyForm(forms.Typed): submit = forms.submit("Add SSH key") -class DeleteKeyForm(forms.Typed): - submit = forms.submit("Delete key") +class DeleteOpenPGPKeyForm(form.Form): + variant: DELETE_OPENPGP_KEY = form.value(DELETE_OPENPGP_KEY) + fingerprint: str = form.label("Fingerprint", widget=form.Widget.HIDDEN) + + +class DeleteSSHKeyForm(form.Form): + variant: DELETE_SSH_KEY = form.value(DELETE_SSH_KEY) + fingerprint: str = form.label("Fingerprint", widget=form.Widget.HIDDEN) + + +class UpdateCommitteeKeysForm(form.Empty): + variant: UPDATE_COMMITTEE_KEYS = form.value(UPDATE_COMMITTEE_KEYS) + committee_name: str = form.label("Committee name", widget=form.Widget.HIDDEN) -class UpdateCommitteeKeysForm(forms.Typed): - submit = forms.submit("Regenerate KEYS file") +type KeysForm = Annotated[ + DeleteOpenPGPKeyForm | DeleteSSHKeyForm | UpdateCommitteeKeysForm, + form.DISCRIMINATOR, +] class UpdateKeyCommitteesForm(forms.Typed): diff --git a/atr/templates/committee-view.html b/atr/templates/committee-view.html index f3a0d7f..275f9aa 100644 --- a/atr/templates/committee-view.html +++ b/atr/templates/committee-view.html @@ -77,9 +77,11 @@ The <code>KEYS</code> file is automatically generated when you add or remove a key, but you can also use the form below to manually regenerate it. </p> <form method="post" - action="{{ as_url(post.keys.update_committee_keys, committee_name=committee.name) }}" + action="{{ as_url(post.keys.keys) }}" class="mb-4 d-inline-block"> {{ update_committee_keys_form.hidden_tag() }} + <input type="hidden" name="variant" value="update_committee_keys" /> + <input type="hidden" name="committee_name" value="{{ committee.name }}" /> {{ update_committee_keys_form.submit(class_='btn btn-sm btn-outline-secondary') }} </form> diff --git a/atr/templates/keys-review.html b/atr/templates/keys-review.html deleted file mode 100644 index ff5d441..0000000 --- a/atr/templates/keys-review.html +++ /dev/null @@ -1,161 +0,0 @@ -{% extends "layouts/base.html" %} - -{% block title %} - Manage keys ~ ATR -{% endblock title %} - -{% block description %} - Review your keys. -{% endblock description %} - -{% block content %} - <h1>Manage keys</h1> - - <p class="mb-4"> - <a href="#your-public-keys" class="btn btn-sm btn-secondary me-3">Your public keys</a> - <a href="#your-committee-keys" class="btn btn-sm btn-secondary">Your committee's keys</a> - </p> - - <h2 id="your-public-keys">Your public keys</h2> - <p>Review your public keys used for signing release artifacts.</p> - - <div class="d-flex gap-3 mb-4"> - <a href="{{ as_url(get.keys.add) }}" class="btn btn-outline-primary">Add your OpenPGP key</a> - <a href="{{ as_url(get.keys.ssh_add) }}" class="btn btn-outline-primary">Add your SSH key</a> - </div> - - <h3>Your OpenPGP keys</h3> - - {% if user_keys %} - <div class="table-responsive mb-5"> - <table class="table border table-striped table-hover table-sm"> - <thead> - <tr> - <th class="px-2" scope="col">Key ID</th> - <th class="px-2" scope="col">Committees</th> - <th class="px-2" scope="col">Action</th> - </tr> - </thead> - <tbody> - {% for key in user_keys %} - <tr class="page-user-openpgp-key"> - <td class="text-break px-2 align-middle"> - <a href="{{ as_url(get.keys.details, fingerprint=key.fingerprint) }}">{{ key.fingerprint[-16:]|upper }}</a> - </td> - <td class="text-break px-2 align-middle"> - {% if key.committees %} - {{ key.committees|map(attribute='name') |join(', ') }} - {% else %} - No PMCs associated - {% endif %} - </td> - <td class="px-2"> - <form method="post" - action="{{ as_url(post.keys.delete) }}" - class="m-0" - onsubmit="return confirm('Are you sure you want to delete this OpenPGP key?');"> - {{ delete_form.hidden_tag() }} - <input type="hidden" name="fingerprint" value="{{ key.fingerprint }}" /> - {{ delete_form.submit(class_='btn btn-sm btn-danger', value='Delete key') }} - </form> - </td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - {% else %} - <p> - <strong>You haven't added any personal OpenPGP keys yet.</strong> - </p> - {% endif %} - - <h3>Your SSH keys</h3> - {% if user_ssh_keys %} - <div class="mb-5 p-4 bg-light rounded"> - <div class="d-grid gap-4"> - {% for key in user_ssh_keys %} - <div id="ssh-key-{{ key.fingerprint }}" class="card p-3 border"> - <table class="mb-0"> - <tbody> - <tr> - <th class="p-2 text-dark">Fingerprint</th> - <td class="text-break">{{ key.fingerprint }}</td> - </tr> - <tr> - <th class="p-2 text-dark">Type</th> - <td class="text-break">{{ key.key.split()[0] }}</td> - </tr> - </tbody> - </table> - - <details class="mt-3 p-3 bg-light rounded"> - <summary class="fw-bold">View whole key</summary> - <pre class="mt-3">{{ key.key }}</pre> - </details> - - <form method="post" - action="{{ as_url(post.keys.delete) }}" - class="mt-3" - onsubmit="return confirm('Are you sure you want to delete this SSH key?');"> - {{ delete_form.hidden_tag() }} - - <input type="hidden" name="fingerprint" value="{{ key.fingerprint }}" /> - {{ delete_form.submit(class_='btn btn-danger', value='Delete key') }} - </form> - </div> - {% endfor %} - </div> - </div> - {% else %} - <p> - <strong>You haven't added any SSH keys yet.</strong> - </p> - {% endif %} - - <h2 id="your-committee-keys">Your committee's keys</h2> - <div class="mb-4"> - <a href="{{ as_url(get.keys.upload) }}" class="btn btn-outline-primary">Upload a KEYS file</a> - </div> - {% for committee in committees %} - {% if not committee_is_standing(committee.name) %} - <h3 id="committee-{{ committee.name|slugify }}" class="mt-3">{{ committee.display_name }}</h3> - {% if committee.public_signing_keys %} - <div class="table-responsive mb-2"> - <table class="table border table-striped table-hover table-sm"> - <thead> - <tr> - <th class="px-2" scope="col">Key ID</th> - <th class="px-2" scope="col">Email</th> - <th class="px-2" scope="col">Apache UID</th> - </tr> - </thead> - <tbody> - {% for key in committee.public_signing_keys %} - <tr> - <td class="text-break font-monospace px-2"> - <a href="{{ as_url(get.keys.details, fingerprint=key.fingerprint) }}">{{ key.fingerprint[-16:]|upper }}</a> - </td> - <td class="text-break px-2">{{ email_from_key(key.primary_declared_uid) or 'Not specified' }}</td> - <td class="text-break px-2">{{ key.apache_uid or "-" }}</td> - </tr> - {% endfor %} - </tbody> - </table> - </div> - <p class="text-muted"> - The <code>KEYS</code> file is automatically generated when you add or remove a key, but you can also use the form below to manually regenerate it. - </p> - <form method="post" - action="{{ as_url(post.keys.update_committee_keys, committee_name=committee.name) }}" - class="mb-4 d-inline-block"> - {{ update_committee_keys_form.hidden_tag() }} - - {{ update_committee_keys_form.submit(class_='btn btn-sm btn-outline-secondary') }} - </form> - {% else %} - <p class="mb-4">No keys uploaded for this committee yet.</p> - {% endif %} - {% endif %} - {% endfor %} -{% endblock content %} diff --git a/playwright/test.py b/playwright/test.py index daac217..e9e49d0 100755 --- a/playwright/test.py +++ b/playwright/test.py @@ -780,14 +780,14 @@ def test_openpgp_01_upload(page: sync_api.Page, credentials: Credentials) -> Non select_all_button_locator.click() logging.info("Submitting the Add OpenPGP key form") - submit_button_locator = page.locator('input[type="submit"][value="Add OpenPGP key"]') + submit_button_locator = page.get_by_role("button", name="Add OpenPGP key") sync_api.expect(submit_button_locator).to_be_enabled() submit_button_locator.click() logging.info("Waiting for navigation back to /keys page") - wait_for_path(page, "/keys/add") + wait_for_path(page, "/keys") - logging.info("Checking for success flash message on /keys/add page") + logging.info("Checking for success flash message on /keys page") try: flash_message_locator = page.locator("div.flash-success") sync_api.expect(flash_message_locator).to_be_visible() @@ -803,9 +803,6 @@ def test_openpgp_01_upload(page: sync_api.Page, credentials: Credentials) -> Non ) logging.info("OpenPGP key already in database message shown") - logging.info("Navigating back to /keys to verify key presence") - go_to_path(page, "/keys") - logging.info(f"Verifying OpenPGP key with fingerprint {key_fingerprint_upper} is visible") key_row_locator = page.locator(f'tr.page-user-openpgp-key:has(a[href="/keys/details/{key_fingerprint_lower}"])') sync_api.expect(key_row_locator).to_be_visible() @@ -1204,7 +1201,7 @@ def test_tidy_up_openpgp_keys_continued(page: sync_api.Page, fingerprints_to_del # Locate again by fingerprint for robustness row_to_delete_locator = page.locator(f'tr:has(a[href="/keys/details/{fingerprint}"])') delete_button_locator = row_to_delete_locator.locator( - 'form[action="/keys/delete"] input[type="submit"][value="Delete key"]' + 'form[action="/keys"] input[type="submit"][value="Delete key"]' ) if delete_button_locator.is_visible(): @@ -1351,7 +1348,7 @@ def test_tidy_up_ssh_keys_continued(page: sync_api.Page, fingerprints_to_delete: # Locate again by fingerprint for robustness in case of changes card_to_delete_locator = page.locator(f"div.card:has(td:has-text('{fingerprint}'))") delete_button_locator = card_to_delete_locator.locator( - 'form[action="/keys/delete"] input[type="submit"][value="Delete key"]' + 'form[action="/keys"] input[type="submit"][value="Delete key"]' ) if delete_button_locator.is_visible(): --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
