BTHLABS-65: Implement support for Win 11 payload in PWA share sheet endpoint

Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
This commit is contained in:
2025-11-12 19:30:33 +00:00
committed by Tomek Wójcik
parent ac9c7a81c3
commit b358ef6686
7 changed files with 81 additions and 49 deletions

View File

@@ -78,9 +78,13 @@ urlpatterns = [
name='ui.integrations.ios.shortcut', name='ui.integrations.ios.shortcut',
), ),
path( 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/',
integrations.android.share_sheet, integrations.pwa.share_sheet,
name='ui.integrations.android.share_sheet', name='ui.integrations.pwa.share_sheet',
), ),
path( path(
'integrations/extension/authenticate/', 'integrations/extension/authenticate/',

View File

@@ -1,3 +1,3 @@
from . import android # noqa: F401
from . import extension # noqa: F401 from . import extension # noqa: F401
from . import ios # noqa: F401 from . import ios # noqa: F401
from . import pwa # noqa: F401

View File

@@ -21,10 +21,14 @@ def share_sheet(request: HttpRequest) -> HttpResponse:
try: try:
assert request.user.is_anonymous is False, 'Login required' 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() url: str = ''
assert url != '', 'Bad request: Empty `text`' 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( return CreateSaveWorkflow().run(
request=request, request=request,

View File

@@ -50,7 +50,7 @@ def manifest_json(request: HttpRequest) -> JsonResponse:
'scope': '/', 'scope': '/',
'share_target': { 'share_target': {
'action': request.build_absolute_uri( 'action': request.build_absolute_uri(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
), ),
'method': 'POST', 'method': 'POST',
'enctype': 'multipart/form-data', 'enctype': 'multipart/form-data',

View File

@@ -29,21 +29,45 @@ def mock_saves_process_save_task_apply_async(mocker: pytest_mock.MockerFixture,
@pytest.fixture @pytest.fixture
def payload(): def url_to_save():
return 'https://www.ziomek.dog/'
@pytest.fixture
def payload_with_text(url_to_save):
return { 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 @pytest.mark.django_db
def test_ok(authenticated_client: Client, def test_ok(payload_fixture_name,
payload, request: pytest.FixtureRequest,
authenticated_client: Client,
account, account,
url_to_save,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given
payload = request.getfixturevalue(payload_fixture_name)
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload,
) )
@@ -69,7 +93,7 @@ def test_ok(authenticated_client: Client,
SavesTestingService().assert_created( SavesTestingService().assert_created(
pk=save_pk, pk=save_pk,
account_uuid=account.pk, account_uuid=account.pk,
url=payload['text'], url=url_to_save,
is_netloc_banned=False, is_netloc_banned=False,
) )
@@ -82,17 +106,17 @@ def test_ok(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_ok_netloc_banned(authenticated_client: Client, def test_ok_netloc_banned(authenticated_client: Client,
payload, payload_with_text,
account, account,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
payload['text'] = 'https://youtube.com/' payload_with_text['text'] = 'https://youtube.com/'
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -108,25 +132,25 @@ def test_ok_netloc_banned(authenticated_client: Client,
SavesTestingService().assert_created( SavesTestingService().assert_created(
pk=save_pk, pk=save_pk,
account_uuid=account.pk, account_uuid=account.pk,
url=payload['text'], url=payload_with_text['text'],
is_netloc_banned=True, is_netloc_banned=True,
) )
@pytest.mark.django_db @pytest.mark.django_db
def test_ok_reuse_save(authenticated_client: Client, def test_ok_reuse_save(authenticated_client: Client,
payload, payload_with_text,
save_out, save_out,
account, account,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
payload['text'] = save_out.url payload_with_text['text'] = save_out.url
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -156,19 +180,19 @@ def test_ok_reuse_save(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_ok_reuse_association(authenticated_client: Client, def test_ok_reuse_association(authenticated_client: Client,
payload, payload_with_text,
save_out, save_out,
account, account,
association_out, association_out,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
payload['text'] = save_out.url payload_with_text['text'] = save_out.url
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -182,18 +206,18 @@ def test_ok_reuse_association(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_ok_reuse_other_account_save(authenticated_client: Client, def test_ok_reuse_other_account_save(authenticated_client: Client,
payload, payload_with_text,
other_account_save_out, other_account_save_out,
account, account,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
payload['text'] = other_account_save_out.url payload_with_text['text'] = other_account_save_out.url
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -214,18 +238,18 @@ def test_ok_reuse_other_account_save(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_ok_dont_process_reused_processed_save(authenticated_client: Client, def test_ok_dont_process_reused_processed_save(authenticated_client: Client,
payload, payload_with_text,
processed_save_out, processed_save_out,
account, account,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
payload['text'] = processed_save_out.url payload_with_text['text'] = processed_save_out.url
# When # When
_ = authenticated_client.post( _ = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -234,19 +258,19 @@ def test_ok_dont_process_reused_processed_save(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_invalid_all_empty(authenticated_client: Client, def test_invalid_all_empty(authenticated_client: Client,
payload, payload_with_text,
mock_saves_process_save_task_apply_async: mock.Mock, mock_saves_process_save_task_apply_async: mock.Mock,
): ):
# Given # Given
effective_payload = { effective_payload = {
key: '' key: ''
for key for key
in payload.keys() in payload_with_text.keys()
} }
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=effective_payload, data=effective_payload,
) )
@@ -269,7 +293,7 @@ def test_invalid_all_missing(authenticated_client: Client,
# When # When
result = authenticated_client.post( result = authenticated_client.post(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=effective_payload, data=effective_payload,
) )
@@ -285,12 +309,12 @@ def test_invalid_all_missing(authenticated_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_inactive_account(inactive_account_client: Client, def test_inactive_account(inactive_account_client: Client,
payload, payload_with_text,
): ):
# When # When
result = inactive_account_client.get( result = inactive_account_client.get(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -302,8 +326,8 @@ def test_inactive_account(inactive_account_client: Client,
( (
'next', 'next',
reverse( reverse(
'ui.integrations.android.share_sheet', 'ui.integrations.pwa.share_sheet',
query=payload, query=payload_with_text,
), ),
), ),
], ],
@@ -314,12 +338,12 @@ def test_inactive_account(inactive_account_client: Client,
@pytest.mark.django_db @pytest.mark.django_db
def test_anonymous(client: Client, def test_anonymous(client: Client,
payload, payload_with_text,
): ):
# When # When
result = client.get( result = client.get(
reverse('ui.integrations.android.share_sheet'), reverse('ui.integrations.pwa.share_sheet'),
data=payload, data=payload_with_text,
) )
# Then # Then
@@ -331,8 +355,8 @@ def test_anonymous(client: Client,
( (
'next', 'next',
reverse( reverse(
'ui.integrations.android.share_sheet', 'ui.integrations.pwa.share_sheet',
query=payload, query=payload_with_text,
), ),
), ),
], ],

View File

@@ -20,5 +20,5 @@ def test_ok(client: Client, settings):
assert payload['short_name'] == settings.SITE_SHORT_TITLE assert payload['short_name'] == settings.SITE_SHORT_TITLE
assert payload['start_url'] == f"http://testserver{reverse('ui.associations.browse')}" assert payload['start_url'] == f"http://testserver{reverse('ui.associations.browse')}"
assert payload['share_target']['action'] == ( assert payload['share_target']['action'] == (
f"http://testserver{reverse('ui.integrations.android.share_sheet')}" f"http://testserver{reverse('ui.integrations.pwa.share_sheet')}"
) )