From b358ef6686b1716dfdeb10359527a9f34557f894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomek=20W=C3=B3jcik?= Date: Wed, 12 Nov 2025 19:30:33 +0000 Subject: [PATCH] BTHLABS-65: Implement support for Win 11 payload in PWA share sheet endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomek Wójcik Co-committed-by: Tomek Wójcik --- .../backend/hotpocket_backend/apps/ui/urls.py | 8 +- .../apps/ui/views/integrations/__init__.py | 2 +- .../views/integrations/{android.py => pwa.py} | 10 +- .../hotpocket_backend/apps/ui/views/meta.py | 2 +- .../integrations/{android => pwa}/__init__.py | 0 .../{android => pwa}/test_share_sheet.py | 106 +++++++++++------- .../tests/ui/views/meta/test_manifest_json.py | 2 +- 7 files changed, 81 insertions(+), 49 deletions(-) rename services/backend/hotpocket_backend/apps/ui/views/integrations/{android.py => pwa.py} (81%) rename services/backend/tests/ui/views/integrations/{android => pwa}/__init__.py (100%) rename services/backend/tests/ui/views/integrations/{android => pwa}/test_share_sheet.py (78%) diff --git a/services/backend/hotpocket_backend/apps/ui/urls.py b/services/backend/hotpocket_backend/apps/ui/urls.py index ce420f7..1fa047f 100644 --- a/services/backend/hotpocket_backend/apps/ui/urls.py +++ b/services/backend/hotpocket_backend/apps/ui/urls.py @@ -78,9 +78,13 @@ urlpatterns = [ name='ui.integrations.ios.shortcut', ), path( + # Turns out PWAs can register a share target in Windows 11 when + # installed through Edge. Neat, too. I wish I knew this when I defined + # this URL path. Now it's gonna stay forever like this due to backwards + # compat ;). 'integrations/android/share-sheet/', - integrations.android.share_sheet, - name='ui.integrations.android.share_sheet', + integrations.pwa.share_sheet, + name='ui.integrations.pwa.share_sheet', ), path( 'integrations/extension/authenticate/', diff --git a/services/backend/hotpocket_backend/apps/ui/views/integrations/__init__.py b/services/backend/hotpocket_backend/apps/ui/views/integrations/__init__.py index 718f52a..997f90f 100644 --- a/services/backend/hotpocket_backend/apps/ui/views/integrations/__init__.py +++ b/services/backend/hotpocket_backend/apps/ui/views/integrations/__init__.py @@ -1,3 +1,3 @@ -from . import android # noqa: F401 from . import extension # noqa: F401 from . import ios # noqa: F401 +from . import pwa # noqa: F401 diff --git a/services/backend/hotpocket_backend/apps/ui/views/integrations/android.py b/services/backend/hotpocket_backend/apps/ui/views/integrations/pwa.py similarity index 81% rename from services/backend/hotpocket_backend/apps/ui/views/integrations/android.py rename to services/backend/hotpocket_backend/apps/ui/views/integrations/pwa.py index c6765ed..f4b1b26 100644 --- a/services/backend/hotpocket_backend/apps/ui/views/integrations/android.py +++ b/services/backend/hotpocket_backend/apps/ui/views/integrations/pwa.py @@ -21,10 +21,14 @@ def share_sheet(request: HttpRequest) -> HttpResponse: try: assert request.user.is_anonymous is False, 'Login required' - assert 'text' in request.POST, 'Bad request: Missing `text`' - url = request.POST['text'].split('\n')[0].strip() - assert url != '', 'Bad request: Empty `text`' + url: str = '' + if 'url' in request.POST: + url = request.POST['url'].strip() + elif 'text' in request.POST: + url = request.POST['text'].split('\n')[0].strip() + + assert url != '', 'Bad request: Empty `url`' return CreateSaveWorkflow().run( request=request, diff --git a/services/backend/hotpocket_backend/apps/ui/views/meta.py b/services/backend/hotpocket_backend/apps/ui/views/meta.py index aa1c597..727e1a1 100644 --- a/services/backend/hotpocket_backend/apps/ui/views/meta.py +++ b/services/backend/hotpocket_backend/apps/ui/views/meta.py @@ -50,7 +50,7 @@ def manifest_json(request: HttpRequest) -> JsonResponse: 'scope': '/', 'share_target': { 'action': request.build_absolute_uri( - reverse('ui.integrations.android.share_sheet'), + reverse('ui.integrations.pwa.share_sheet'), ), 'method': 'POST', 'enctype': 'multipart/form-data', diff --git a/services/backend/tests/ui/views/integrations/android/__init__.py b/services/backend/tests/ui/views/integrations/pwa/__init__.py similarity index 100% rename from services/backend/tests/ui/views/integrations/android/__init__.py rename to services/backend/tests/ui/views/integrations/pwa/__init__.py diff --git a/services/backend/tests/ui/views/integrations/android/test_share_sheet.py b/services/backend/tests/ui/views/integrations/pwa/test_share_sheet.py similarity index 78% rename from services/backend/tests/ui/views/integrations/android/test_share_sheet.py rename to services/backend/tests/ui/views/integrations/pwa/test_share_sheet.py index 6bdf037..8fabf75 100644 --- a/services/backend/tests/ui/views/integrations/android/test_share_sheet.py +++ b/services/backend/tests/ui/views/integrations/pwa/test_share_sheet.py @@ -29,21 +29,45 @@ def mock_saves_process_save_task_apply_async(mocker: pytest_mock.MockerFixture, @pytest.fixture -def payload(): +def url_to_save(): + return 'https://www.ziomek.dog/' + + +@pytest.fixture +def payload_with_text(url_to_save): return { - 'text': 'https://www.ziomek.dog/', + 'text': url_to_save, } +@pytest.fixture +def payload_with_url(url_to_save): + return { + 'url': url_to_save, + } + + +@pytest.mark.parametrize( + 'payload_fixture_name', + [ + 'payload_with_text', + 'payload_with_url', + ], +) @pytest.mark.django_db -def test_ok(authenticated_client: Client, - payload, +def test_ok(payload_fixture_name, + request: pytest.FixtureRequest, + authenticated_client: Client, account, + url_to_save, mock_saves_process_save_task_apply_async: mock.Mock, ): + # Given + payload = request.getfixturevalue(payload_fixture_name) + # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), + reverse('ui.integrations.pwa.share_sheet'), data=payload, ) @@ -69,7 +93,7 @@ def test_ok(authenticated_client: Client, SavesTestingService().assert_created( pk=save_pk, account_uuid=account.pk, - url=payload['text'], + url=url_to_save, is_netloc_banned=False, ) @@ -82,17 +106,17 @@ def test_ok(authenticated_client: Client, @pytest.mark.django_db def test_ok_netloc_banned(authenticated_client: Client, - payload, + payload_with_text, account, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given - payload['text'] = 'https://youtube.com/' + payload_with_text['text'] = 'https://youtube.com/' # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -108,25 +132,25 @@ def test_ok_netloc_banned(authenticated_client: Client, SavesTestingService().assert_created( pk=save_pk, account_uuid=account.pk, - url=payload['text'], + url=payload_with_text['text'], is_netloc_banned=True, ) @pytest.mark.django_db def test_ok_reuse_save(authenticated_client: Client, - payload, + payload_with_text, save_out, account, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given - payload['text'] = save_out.url + payload_with_text['text'] = save_out.url # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -156,19 +180,19 @@ def test_ok_reuse_save(authenticated_client: Client, @pytest.mark.django_db def test_ok_reuse_association(authenticated_client: Client, - payload, + payload_with_text, save_out, account, association_out, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given - payload['text'] = save_out.url + payload_with_text['text'] = save_out.url # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -182,18 +206,18 @@ def test_ok_reuse_association(authenticated_client: Client, @pytest.mark.django_db def test_ok_reuse_other_account_save(authenticated_client: Client, - payload, + payload_with_text, other_account_save_out, account, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given - payload['text'] = other_account_save_out.url + payload_with_text['text'] = other_account_save_out.url # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -214,18 +238,18 @@ def test_ok_reuse_other_account_save(authenticated_client: Client, @pytest.mark.django_db def test_ok_dont_process_reused_processed_save(authenticated_client: Client, - payload, + payload_with_text, processed_save_out, account, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given - payload['text'] = processed_save_out.url + payload_with_text['text'] = processed_save_out.url # When _ = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -234,19 +258,19 @@ def test_ok_dont_process_reused_processed_save(authenticated_client: Client, @pytest.mark.django_db def test_invalid_all_empty(authenticated_client: Client, - payload, + payload_with_text, mock_saves_process_save_task_apply_async: mock.Mock, ): # Given effective_payload = { key: '' for key - in payload.keys() + in payload_with_text.keys() } # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), + reverse('ui.integrations.pwa.share_sheet'), data=effective_payload, ) @@ -269,7 +293,7 @@ def test_invalid_all_missing(authenticated_client: Client, # When result = authenticated_client.post( - reverse('ui.integrations.android.share_sheet'), + reverse('ui.integrations.pwa.share_sheet'), data=effective_payload, ) @@ -285,12 +309,12 @@ def test_invalid_all_missing(authenticated_client: Client, @pytest.mark.django_db def test_inactive_account(inactive_account_client: Client, - payload, + payload_with_text, ): # When result = inactive_account_client.get( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -302,8 +326,8 @@ def test_inactive_account(inactive_account_client: Client, ( 'next', reverse( - 'ui.integrations.android.share_sheet', - query=payload, + 'ui.integrations.pwa.share_sheet', + query=payload_with_text, ), ), ], @@ -314,12 +338,12 @@ def test_inactive_account(inactive_account_client: Client, @pytest.mark.django_db def test_anonymous(client: Client, - payload, + payload_with_text, ): # When result = client.get( - reverse('ui.integrations.android.share_sheet'), - data=payload, + reverse('ui.integrations.pwa.share_sheet'), + data=payload_with_text, ) # Then @@ -331,8 +355,8 @@ def test_anonymous(client: Client, ( 'next', reverse( - 'ui.integrations.android.share_sheet', - query=payload, + 'ui.integrations.pwa.share_sheet', + query=payload_with_text, ), ), ], diff --git a/services/backend/tests/ui/views/meta/test_manifest_json.py b/services/backend/tests/ui/views/meta/test_manifest_json.py index 7d16839..9fbe110 100644 --- a/services/backend/tests/ui/views/meta/test_manifest_json.py +++ b/services/backend/tests/ui/views/meta/test_manifest_json.py @@ -20,5 +20,5 @@ def test_ok(client: Client, settings): assert payload['short_name'] == settings.SITE_SHORT_TITLE assert payload['start_url'] == f"http://testserver{reverse('ui.associations.browse')}" assert payload['share_target']['action'] == ( - f"http://testserver{reverse('ui.integrations.android.share_sheet')}" + f"http://testserver{reverse('ui.integrations.pwa.share_sheet')}" )