remote_billing: Make "plan management" always available.

Just shows a config error page if the bouncer is not enabled. Uses a new
endpoint for this so that it can work nicely for both browser and
desktop app clients.
It's necessary, because the desktop app expects to get a json response
with either an error or billing_access_url to redirect to. Showing a
nice config error page can't be done via the json error mechanism, so
instead we just serve a redirect to the new error page, which the app
will open in the browser in a new window or tab.
This commit is contained in:
Mateusz Mandera 2024-02-28 20:03:34 +01:00 committed by Tim Abbott
parent d5fb3f3176
commit e39f400f94
7 changed files with 115 additions and 18 deletions

View File

@ -199,12 +199,41 @@ class SelfHostedBillingEndpointBasicTest(RemoteRealmBillingTestCase):
self_hosted_billing_json_url = "/json/self-hosted-billing" self_hosted_billing_json_url = "/json/self-hosted-billing"
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=None): with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=None):
result = self.client_get(self_hosted_billing_url) with self.settings(CORPORATE_ENABLED=True):
self.assertEqual(result.status_code, 404) result = self.client_get(self_hosted_billing_url)
self.assert_in_response("Page not found (404)", result) self.assertEqual(result.status_code, 404)
self.assert_in_response("Page not found (404)", result)
result = self.client_get(self_hosted_billing_json_url) with self.settings(CORPORATE_ENABLED=False):
self.assert_json_error(result, "Server doesn't use the push notification service", 404) result = self.client_get(self_hosted_billing_url)
self.assertEqual(result.status_code, 302)
redirect_url = result["Location"]
self.assertEqual(redirect_url, "/self-hosted-billing/not-configured/")
with self.assertLogs("django.request"):
result = self.client_get(redirect_url)
self.assert_in_response(
"This server is not configured to use push notifications.", result
)
with self.settings(CORPORATE_ENABLED=True):
result = self.client_get(self_hosted_billing_json_url)
self.assert_json_error(
result, "Server doesn't use the push notification service", 404
)
with self.settings(CORPORATE_ENABLED=False):
result = self.client_get(self_hosted_billing_json_url)
self.assert_json_success(result)
redirect_url = result.json()["billing_access_url"]
self.assertEqual(redirect_url, "/self-hosted-billing/not-configured/")
with self.assertLogs("django.request"):
result = self.client_get(redirect_url)
self.assert_in_response(
"This server is not configured to use push notifications.", result
)
with mock.patch( with mock.patch(
"zerver.views.push_notifications.send_to_push_bouncer", "zerver.views.push_notifications.send_to_push_bouncer",
@ -240,6 +269,26 @@ class SelfHostedBillingEndpointBasicTest(RemoteRealmBillingTestCase):
@override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com") @override_settings(PUSH_NOTIFICATION_BOUNCER_URL="https://push.zulip.org.example.com")
class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase): class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase):
def test_self_hosted_config_error_page(self) -> None:
with self.settings(
CORPORATE_ENABLED=False, PUSH_NOTIFICATION_BOUNCER_URL=None
), self.assertLogs("django.request"):
result = self.client_get("/self-hosted-billing/not-configured/")
self.assertEqual(result.status_code, 500)
self.assert_in_response(
"This server is not configured to use push notifications.", result
)
# The page doesn't make sense if PUSH_NOTIFICATION_BOUNCER_URL is configured.
with self.settings(CORPORATE_ENABLED=False):
result = self.client_get("/self-hosted-billing/not-configured/")
self.assertEqual(result.status_code, 404)
# Also doesn't make sense on zulipchat.com (where CORPORATE_ENABLED is True).
with self.settings(CORPORATE_ENABLED=True, PUSH_NOTIFICATION_BOUNCER_URL=None):
result = self.client_get("/self-hosted-billing/not-configured/")
self.assertEqual(result.status_code, 404)
@responses.activate @responses.activate
def test_remote_billing_authentication_flow(self) -> None: def test_remote_billing_authentication_flow(self) -> None:
self.login("desdemona") self.login("desdemona")

View File

@ -0,0 +1,9 @@
{% extends "zerver/config_error/container.html" %}
{% block error_content %}
{% trans doc_url="https://zulip.readthedocs.io/en/latest/production/mobile-push-notifications.html" %}
This server is not configured to use push notifications. For instructions on how to
configure push notifications, please see the
<a href="{{ doc_url }}">documentation</a>.
{% endtrans %}
{% endblock %}

View File

@ -16,7 +16,6 @@ from zerver.lib.i18n import (
get_language_translation_data, get_language_translation_data,
) )
from zerver.lib.narrow_helpers import NarrowTerm from zerver.lib.narrow_helpers import NarrowTerm
from zerver.lib.push_notifications import uses_notification_bouncer
from zerver.lib.realm_description import get_realm_rendered_description from zerver.lib.realm_description import get_realm_rendered_description
from zerver.lib.request import RequestNotes from zerver.lib.request import RequestNotes
from zerver.models import Message, Realm, Stream, UserProfile from zerver.models import Message, Realm, Stream, UserProfile
@ -82,8 +81,13 @@ def get_billing_info(user_profile: Optional[UserProfile]) -> BillingInfo:
show_billing = False show_billing = False
show_plans = False show_plans = False
sponsorship_pending = False sponsorship_pending = False
# We want to always show the remote billing option as long as the user is authorized,
# except on zulipchat.com where it's not applicable.
show_remote_billing = ( show_remote_billing = (
user_profile is not None and user_profile.has_billing_access and uses_notification_bouncer() (not settings.CORPORATE_ENABLED)
and user_profile is not None
and user_profile.has_billing_access
) )
# This query runs on home page load, so we want to avoid # This query runs on home page load, so we want to avoid

View File

@ -528,6 +528,9 @@ def write_instrumentation_reports(full_suite: bool, include_webhooks: bool) -> N
# TODO: This endpoint and the rest of its system are a work in progress, # TODO: This endpoint and the rest of its system are a work in progress,
# we are not testing it yet. # we are not testing it yet.
"self-hosted-billing/", "self-hosted-billing/",
# This endpoint only returns 500 and 404 codes, so it doesn't get picked up
# by find_pattern above and therefore needs to be exempt.
"self-hosted-billing/not-configured/",
*(webhook.url for webhook in WEBHOOK_INTEGRATIONS if not include_webhooks), *(webhook.url for webhook in WEBHOOK_INTEGRATIONS if not include_webhooks),
} }

View File

@ -973,7 +973,7 @@ class HomeTest(ZulipTestCase):
self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# realm owner, with inactive CustomerPlan and realm plan_type SELF_HOSTED -> show only billing link # realm owner, with inactive CustomerPlan and realm plan_type SELF_HOSTED -> show only billing link
customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id") customer = Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id")
@ -990,7 +990,7 @@ class HomeTest(ZulipTestCase):
self.assertTrue(billing_info.show_billing) self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# realm owner, with inactive CustomerPlan and realm plan_type LIMITED -> show billing link and plans # realm owner, with inactive CustomerPlan and realm plan_type LIMITED -> show billing link and plans
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None) do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_LIMITED, acting_user=None)
@ -999,7 +999,7 @@ class HomeTest(ZulipTestCase):
self.assertTrue(billing_info.show_billing) self.assertTrue(billing_info.show_billing)
self.assertTrue(billing_info.show_plans) self.assertTrue(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# Always false without CORPORATE_ENABLED # Always false without CORPORATE_ENABLED
with self.settings(CORPORATE_ENABLED=False): with self.settings(CORPORATE_ENABLED=False):
@ -1029,6 +1029,12 @@ class HomeTest(ZulipTestCase):
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# Self-hosted servers show remote billing, but not for a user without
# billing access permission.
with self.settings(CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_remote_billing)
# billing admin, with CustomerPlan and realm plan_type STANDARD -> show only billing link # billing admin, with CustomerPlan and realm plan_type STANDARD -> show only billing link
user.role = UserProfile.ROLE_MEMBER user.role = UserProfile.ROLE_MEMBER
user.is_billing_admin = True user.is_billing_admin = True
@ -1039,6 +1045,11 @@ class HomeTest(ZulipTestCase):
self.assertTrue(billing_info.show_billing) self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertFalse(billing_info.show_remote_billing)
# Self-hosted servers show remote billing for billing admins.
with self.settings(CORPORATE_ENABLED=False):
billing_info = get_billing_info(user)
self.assertTrue(billing_info.show_remote_billing) self.assertTrue(billing_info.show_remote_billing)
# billing admin, with CustomerPlan and realm plan_type PLUS -> show only billing link # billing admin, with CustomerPlan and realm plan_type PLUS -> show only billing link
@ -1049,7 +1060,7 @@ class HomeTest(ZulipTestCase):
self.assertTrue(billing_info.show_billing) self.assertTrue(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# member, with CustomerPlan and realm plan_type STANDARD -> neither billing link or plans # member, with CustomerPlan and realm plan_type STANDARD -> neither billing link or plans
do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None) do_change_realm_plan_type(user.realm, Realm.PLAN_TYPE_STANDARD, acting_user=None)
@ -1083,7 +1094,7 @@ class HomeTest(ZulipTestCase):
self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# billing admin, with sponsorship pending and realm plan_type SELF_HOSTED -> show only sponsorship pending link # billing admin, with sponsorship pending and realm plan_type SELF_HOSTED -> show only sponsorship pending link
customer.sponsorship_pending = True customer.sponsorship_pending = True
@ -1093,7 +1104,7 @@ class HomeTest(ZulipTestCase):
self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertTrue(billing_info.sponsorship_pending) self.assertTrue(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# billing admin, no customer object and realm plan_type SELF_HOSTED -> no links # billing admin, no customer object and realm plan_type SELF_HOSTED -> no links
customer.delete() customer.delete()
@ -1102,13 +1113,14 @@ class HomeTest(ZulipTestCase):
self.assertFalse(billing_info.show_billing) self.assertFalse(billing_info.show_billing)
self.assertFalse(billing_info.show_plans) self.assertFalse(billing_info.show_plans)
self.assertFalse(billing_info.sponsorship_pending) self.assertFalse(billing_info.sponsorship_pending)
self.assertTrue(billing_info.show_remote_billing) self.assertFalse(billing_info.show_remote_billing)
# If the server doesn't have the push bouncer configured, # If the server doesn't have the push bouncer configured,
# don't show remote billing. # remote billing should be shown anyway, as the billing endpoint
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=None): # is supposed show a useful error page.
with self.settings(PUSH_NOTIFICATION_BOUNCER_URL=None, CORPORATE_ENABLED=False):
billing_info = get_billing_info(user) billing_info = get_billing_info(user)
self.assertFalse(billing_info.show_remote_billing) self.assertTrue(billing_info.show_remote_billing)
def test_promote_sponsoring_zulip_in_realm(self) -> None: def test_promote_sponsoring_zulip_in_realm(self) -> None:
realm = get_realm("zulip") realm = get_realm("zulip")

View File

@ -4,6 +4,7 @@ from typing import Optional
from django.conf import settings from django.conf import settings
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
from django.shortcuts import render from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from zerver.decorator import human_users_only, zulip_login_required from zerver.decorator import human_users_only, zulip_login_required
@ -33,6 +34,7 @@ from zerver.lib.response import json_success
from zerver.lib.typed_endpoint import typed_endpoint from zerver.lib.typed_endpoint import typed_endpoint
from zerver.lib.validator import check_string from zerver.lib.validator import check_string
from zerver.models import PushDeviceToken, UserProfile from zerver.models import PushDeviceToken, UserProfile
from zerver.views.errors import config_error
def check_app_id(var_name: str, val: object) -> str: def check_app_id(var_name: str, val: object) -> str:
@ -123,7 +125,11 @@ def self_hosting_auth_view_common(
request: HttpRequest, user_profile: UserProfile, next_page: Optional[str] = None request: HttpRequest, user_profile: UserProfile, next_page: Optional[str] = None
) -> str: ) -> str:
if not uses_notification_bouncer(): if not uses_notification_bouncer():
raise ResourceNotFoundError(_("Server doesn't use the push notification service")) if settings.CORPORATE_ENABLED:
# This endpoint makes no sense on zulipchat.com, so just 404.
raise ResourceNotFoundError(_("Server doesn't use the push notification service"))
else:
return reverse(self_hosting_auth_not_configured)
if not user_profile.has_billing_access: # nocoverage if not user_profile.has_billing_access: # nocoverage
# We may want to replace this with an html error page at some point, # We may want to replace this with an html error page at some point,
@ -210,3 +216,12 @@ def self_hosting_auth_json_endpoint(
redirect_url = self_hosting_auth_view_common(request, user_profile, next_page) redirect_url = self_hosting_auth_view_common(request, user_profile, next_page)
return json_success(request, data={"billing_access_url": redirect_url}) return json_success(request, data={"billing_access_url": redirect_url})
def self_hosting_auth_not_configured(request: HttpRequest) -> HttpResponse:
if settings.CORPORATE_ENABLED or uses_notification_bouncer():
# This error page should only be available if the config error
# is actually real.
return render(request, "404.html", status=404)
return config_error(request, "remote_billing_bouncer_not_configured")

View File

@ -99,6 +99,7 @@ from zerver.views.push_notifications import (
remove_android_reg_id, remove_android_reg_id,
remove_apns_device_token, remove_apns_device_token,
self_hosting_auth_json_endpoint, self_hosting_auth_json_endpoint,
self_hosting_auth_not_configured,
self_hosting_auth_redirect_endpoint, self_hosting_auth_redirect_endpoint,
send_test_push_notification_api, send_test_push_notification_api,
) )
@ -828,6 +829,10 @@ urls += [
self_hosting_auth_redirect_endpoint, self_hosting_auth_redirect_endpoint,
name="self_hosting_auth_redirect_endpoint", name="self_hosting_auth_redirect_endpoint",
), ),
path(
"self-hosted-billing/not-configured/",
self_hosting_auth_not_configured,
),
rest_path( rest_path(
"json/self-hosted-billing", "json/self-hosted-billing",
GET=self_hosting_auth_json_endpoint, GET=self_hosting_auth_json_endpoint,