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',
),
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/',

View File

@@ -1,3 +1,3 @@
from . import android # noqa: F401
from . import extension # 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:
assert request.user.is_anonymous is False, 'Login required'
assert 'text' in request.POST, 'Bad request: Missing `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 `text`'
assert url != '', 'Bad request: Empty `url`'
return CreateSaveWorkflow().run(
request=request,

View File

@@ -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',

View File

@@ -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,
),
),
],

View File

@@ -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')}"
)