0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 18:07:17 +01:00
posthog/ee/api/test/test_billing.py
2024-10-10 12:31:55 -04:00

944 lines
37 KiB
Python

from datetime import datetime
from typing import Any
from unittest.mock import MagicMock, patch
from uuid import uuid4
from zoneinfo import ZoneInfo
import jwt
from dateutil.relativedelta import relativedelta
from django.utils.timezone import now
from freezegun import freeze_time
from rest_framework import status
from ee.api.test.base import APILicensedTest
from ee.billing.billing_types import (
BillingPeriod,
CustomerInfo,
CustomerProduct,
CustomerProductAddon,
)
from ee.billing.test.test_billing_manager import create_default_products_response
from ee.models.license import License
from posthog.cloud_utils import (
TEST_clear_instance_license_cache,
get_cached_instance_license,
)
from posthog.models.organization import OrganizationMembership
from posthog.models.team import Team
from posthog.test.base import APIBaseTest, _create_event, flush_persons_and_events
def create_billing_response(**kwargs) -> dict[str, Any]:
data: Any = {"license": {"type": "cloud"}}
data.update(kwargs)
return data
def create_missing_billing_customer(**kwargs) -> CustomerInfo:
data = CustomerInfo(
customer_id="cus_123",
deactivated=False,
custom_limits_usd={},
has_active_subscription=False,
current_total_amount_usd="0.00",
products=None,
billing_period=BillingPeriod(
current_period_start="2022-10-07T11:12:48",
current_period_end="2022-11-07T11:12:48",
),
usage_summary={
"events": {"limit": None, "usage": 0},
"recordings": {"limit": None, "usage": 0},
"rows_synced": {"limit": None, "usage": 0},
},
free_trial_until=None,
available_product_features=[],
)
data.update(kwargs)
return data
def create_billing_customer(**kwargs) -> CustomerInfo:
data = CustomerInfo(
customer_id="cus_123",
custom_limits_usd={},
has_active_subscription=True,
current_total_amount_usd="100.00",
deactivated=False,
products=[
CustomerProduct(
name="Product OS",
description="Product Analytics, event pipelines, data warehousing",
price_description=None,
type="product_analytics",
image_url="https://posthog.com/static/images/product-os.png",
free_allocation=10000,
tiers=[
{
"unit_amount_usd": "0.00",
"up_to": 1000000,
"current_amount_usd": "0.00",
},
{
"unit_amount_usd": "0.00045",
"up_to": 2000000,
"current_amount_usd": "0.00",
},
],
tiered=True,
unit_amount_usd="0.00",
current_amount_usd="0.00",
current_usage=0,
usage_limit=None,
has_exceeded_limit=False,
percentage_usage=0,
projected_usage=0,
projected_amount_usd="0.00",
usage_key="events",
addons=[
CustomerProductAddon(
name="Addon",
description="Test Addon",
price_description=None,
type="addon",
image_url="https://posthog.com/static/images/product-os.png",
free_allocation=10000,
tiers=[
{
"unit_amount_usd": "0.00",
"up_to": 1000000,
"current_amount_usd": "0.00",
},
{
"unit_amount_usd": "0.0000135",
"up_to": 2000000,
"current_amount_usd": "0.00",
},
],
tiered=True,
unit_amount_usd="0.00",
current_amount_usd="0.00",
current_usage=0,
usage_limit=None,
has_exceeded_limit=False,
percentage_usage=0,
projected_usage=0,
projected_amount_usd="0.00",
usage_key="events",
subscribed=True,
)
],
)
],
customer_trust_scores={
"surveys": 15,
"feature_flags": 15,
"data_warehouse": 15,
"session_replay": 15,
"product_analytics": 15,
},
billing_period=BillingPeriod(
current_period_start="2022-10-07T11:12:48",
current_period_end="2022-11-07T11:12:48",
),
usage_summary={
"events": {"limit": None, "usage": 0},
"recordings": {"limit": None, "usage": 0},
"rows_synced": {"limit": None, "usage": 0},
},
free_trial_until=None,
)
data.update(kwargs)
return data
def create_billing_products_response(**kwargs) -> dict[str, list[CustomerProduct]]:
data: Any = {
"products": [
CustomerProduct(
name="Product OS",
description="Product Analytics, event pipelines, data warehousing",
price_description=None,
type="events",
image_url="https://posthog.com/static/images/product-os.png",
free_allocation=10000,
tiers=[
{
"unit_amount_usd": "0.00",
"up_to": 1000000,
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
},
{
"unit_amount_usd": "0.00045",
"up_to": 2000000,
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
},
],
addons=[
{
"current_amount_usd": 0.0,
"current_usage": 0,
"description": "Test Addon",
"free_allocation": 10000,
"has_exceeded_limit": False,
"image_url": "https://posthog.com/static/images/product-os.png",
"name": "Addon",
"percentage_usage": 0,
"price_description": None,
"projected_amount_usd": "0.00",
"projected_usage": 0,
"subscribed": True,
"tiered": True,
"tiers": [
{
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
"unit_amount_usd": "0.00",
"up_to": 1000000,
},
{
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
"unit_amount_usd": "0.0000135",
"up_to": 2000000,
},
],
"type": "events",
"unit_amount_usd": "0.00",
"usage_key": "events",
"usage_limit": None,
},
],
tiered=True,
unit_amount_usd="0.00",
current_amount_usd=0.0,
current_usage=0,
usage_limit=None,
has_exceeded_limit=False,
percentage_usage=0,
projected_usage=0,
projected_amount=0,
projected_amount_usd=0.00,
usage_key="events",
)
]
}
data.update(kwargs)
return data
class TestUnlicensedBillingAPI(APIBaseTest):
@patch("ee.api.billing.requests.get")
@freeze_time("2022-01-01")
def test_billing_calls_the_service_without_token(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 401
mock.json.return_value = {"detail": "Authorization is missing."}
elif "api/products" in url:
mock.status_code = 200
mock.json.return_value = create_default_products_response()
return mock
mock_request.side_effect = mock_implementation
TEST_clear_instance_license_cache()
res = self.client.get("/api/billing")
assert res.status_code == 200
assert res.json() == {
"available_product_features": [],
"products": create_default_products_response()["products"],
}
class TestBillingAPI(APILicensedTest):
def test_billing_fails_for_old_license_type(self):
self.license.key = "test_key"
self.license.save()
TEST_clear_instance_license_cache()
res = self.client.get("/api/billing")
assert res.status_code == 404
assert res.json()["detail"] == "Billing is not supported for this license type"
@patch("ee.api.billing.requests.get")
@freeze_time("2022-01-01")
def test_billing_calls_the_service_with_appropriate_token(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(customer=create_billing_customer())
return mock
mock_request.side_effect = mock_implementation
TEST_clear_instance_license_cache()
self.client.get("/api/billing")
assert mock_request.call_args_list[0].args[0].endswith("/api/billing")
token = mock_request.call_args_list[0].kwargs["headers"]["Authorization"].split(" ")[1]
secret = self.license.key.split("::")[1]
decoded_token = jwt.decode(
token,
secret,
algorithms=["HS256"],
audience="posthog:license-key",
options={"verify_aud": True},
)
assert decoded_token == {
"aud": "posthog:license-key",
"distinct_id": str(self.user.distinct_id),
"exp": 1640996100,
"id": self.license.key.split("::")[0],
"organization_id": str(self.organization.id),
"organization_name": "Test",
}
@patch("ee.api.billing.requests.get")
def test_billing_returns_if_billing_exists(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(customer=create_billing_customer())
return mock
mock_request.side_effect = mock_implementation
TEST_clear_instance_license_cache()
response = self.client.get("/api/billing")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"customer_id": "cus_123",
"customer_id": "cus_123",
"customer_trust_scores": {
"data_warehouse": 15,
"feature_flags": 15,
"product_analytics": 15,
"session_replay": 15,
"surveys": 15,
},
"license": {"plan": "cloud"},
"available_product_features": [],
"custom_limits_usd": {},
"has_active_subscription": True,
"stripe_portal_url": "http://localhost:8000/api/billing/portal",
"current_total_amount_usd": "100.00",
"deactivated": False,
"products": [
{
"name": "Product OS",
"description": "Product Analytics, event pipelines, data warehousing",
"price_description": None,
"type": "product_analytics",
"image_url": "https://posthog.com/static/images/product-os.png",
"free_allocation": 10000,
"tiers": [
{
"unit_amount_usd": "0.00",
"up_to": 1000000,
"current_amount_usd": "0.00",
},
{
"unit_amount_usd": "0.00045",
"up_to": 2000000,
"current_amount_usd": "0.00",
},
],
"tiered": True,
"current_amount_usd": "0.00",
"current_usage": 0,
"usage_limit": None,
"percentage_usage": 0,
"has_exceeded_limit": False,
"unit_amount_usd": "0.00",
"projected_amount_usd": "0.00",
"projected_usage": 0,
"usage_key": "events",
"addons": [
{
"current_amount_usd": "0.00",
"current_usage": 0,
"description": "Test Addon",
"free_allocation": 10000,
"has_exceeded_limit": False,
"image_url": "https://posthog.com/static/images/product-os.png",
"name": "Addon",
"percentage_usage": 0,
"price_description": None,
"projected_amount_usd": "0.00",
"projected_usage": 0,
"subscribed": True,
"tiered": True,
"tiers": [
{
"current_amount_usd": "0.00",
"unit_amount_usd": "0.00",
"up_to": 1000000,
},
{
"current_amount_usd": "0.00",
"unit_amount_usd": "0.0000135",
"up_to": 2000000,
},
],
"type": "addon",
"unit_amount_usd": "0.00",
"usage_key": "events",
"usage_limit": None,
},
],
},
],
"billing_period": {
"current_period_start": "2022-10-07T11:12:48",
"current_period_end": "2022-11-07T11:12:48",
},
"usage_summary": {
"events": {"limit": None, "usage": 0},
"recordings": {"limit": None, "usage": 0},
"rows_synced": {"limit": None, "usage": 0},
},
"free_trial_until": None,
}
@patch("ee.api.billing.requests.get")
def test_billing_returns_if_doesnt_exist(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(customer=create_missing_billing_customer())
elif "api/products" in url:
mock.status_code = 200
mock.json.return_value = create_billing_products_response()
return mock
mock_request.side_effect = mock_implementation
response = self.client.get("/api/billing")
assert response.status_code == status.HTTP_200_OK
assert response.json() == {
"customer_id": "cus_123",
"license": {"plan": "cloud"},
"custom_limits_usd": {},
"has_active_subscription": False,
"available_product_features": [],
"products": [
{
"name": "Product OS",
"description": "Product Analytics, event pipelines, data warehousing",
"price_description": None,
"type": "events",
"free_allocation": 10000,
"tiers": [
{
"unit_amount_usd": "0.00",
"up_to": 1000000,
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
},
{
"unit_amount_usd": "0.00045",
"up_to": 2000000,
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
},
],
"current_usage": 0,
"percentage_usage": 0,
"current_amount_usd": 0.0,
"has_exceeded_limit": False,
"projected_amount_usd": 0.0,
"projected_amount": 0,
"projected_usage": 0,
"tiered": True,
"unit_amount_usd": "0.00",
"usage_limit": None,
"image_url": "https://posthog.com/static/images/product-os.png",
"percentage_usage": 0,
"usage_key": "events",
"addons": [
{
"current_amount_usd": 0.0,
"current_usage": 0,
"description": "Test Addon",
"free_allocation": 10000,
"has_exceeded_limit": False,
"image_url": "https://posthog.com/static/images/product-os.png",
"name": "Addon",
"percentage_usage": 0,
"price_description": None,
"projected_amount_usd": "0.00",
"projected_usage": 0,
"subscribed": True,
"tiered": True,
"tiers": [
{
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
"unit_amount_usd": "0.00",
"up_to": 1000000,
},
{
"current_amount_usd": "0.00",
"current_usage": 0,
"flat_amount_usd": "0",
"projected_amount_usd": "None",
"projected_usage": None,
"unit_amount_usd": "0.0000135",
"up_to": 2000000,
},
],
"type": "events",
"unit_amount_usd": "0.00",
"usage_key": "events",
"usage_limit": None,
},
],
}
],
"billing_period": {
"current_period_start": "2022-10-07T11:12:48",
"current_period_end": "2022-11-07T11:12:48",
},
"usage_summary": {
"events": {"limit": None, "usage": 0},
"recordings": {"limit": None, "usage": 0},
"rows_synced": {"limit": None, "usage": 0},
},
"free_trial_until": None,
"current_total_amount_usd": "0.00",
"deactivated": False,
"stripe_portal_url": "http://localhost:8000/api/billing/portal",
}
@patch("ee.api.billing.requests.get")
def test_billing_stores_valid_license(self, mock_request):
self.license.delete()
mock_request.return_value.status_code = 200
mock_request.return_value.json.return_value = {
"license": {
"type": "scale",
}
}
response = self.client.patch(
"/api/billing/license",
{
"license": "test::test",
},
)
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"success": True}
license = License.objects.first_valid()
assert license
assert license.key == "test::test"
assert license.plan == "scale"
@patch("ee.api.billing.requests.get")
def test_billing_ignores_invalid_license(self, mock_request):
self.license.delete()
mock_request.return_value.status_code = 403
mock_request.return_value.json.return_value = {}
response = self.client.patch(
"/api/billing/license",
{
"license": "test::test",
},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.json() == {
"attr": "license",
"code": "invalid_input",
"detail": "License could not be activated. Please contact support. (BillingService status 403)",
"type": "validation_error",
}
@freeze_time("2022-01-01T12:00:00Z")
@patch("ee.api.billing.requests.get")
def test_license_is_updated_on_billing_load(self, mock_request):
mock_request.return_value.status_code = 200
mock_request.return_value.json.return_value = {
"license": {
"type": "scale",
},
"customer": create_billing_customer(),
}
assert self.license.plan == "enterprise"
self.client.get("/api/billing")
self.license.refresh_from_db()
self.license.valid_until = datetime(2022, 1, 2, 0, 0, 0, tzinfo=ZoneInfo("UTC"))
self.license.save()
assert self.license.plan == "scale"
TEST_clear_instance_license_cache()
license = get_cached_instance_license()
assert license.plan == "scale"
assert license.valid_until == datetime(2022, 1, 2, 0, 0, 0, tzinfo=ZoneInfo("UTC"))
mock_request.return_value.json.return_value = {
"license": {
"type": "enterprise",
},
"customer": create_billing_customer(),
}
self.client.get("/api/billing")
license = get_cached_instance_license()
assert license.plan == "enterprise"
# Should be extended by 30 days
assert license.valid_until == datetime(2022, 1, 31, 12, 0, 0, tzinfo=ZoneInfo("UTC"))
@patch("ee.api.billing.requests.get")
def test_organization_available_product_features_updated_if_different(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(
customer=create_billing_customer(
available_product_features=[
{"key": "feature1", "name": "feature1"},
{"key": "feature2", "name": "feature2"},
]
)
)
return mock
mock_request.side_effect = mock_implementation
self.organization.available_product_features = []
self.organization.save()
assert self.organization.available_product_features == []
self.client.get("/api/billing")
self.organization.refresh_from_db()
assert self.organization.available_product_features == [
{
"key": "feature1",
"name": "feature1",
},
{"key": "feature2", "name": "feature2"},
]
@patch("ee.api.billing.requests.get")
def test_organization_update_usage(self, mock_request):
self.organization.customer_id = None
self.organization.usage = None
self.organization.save()
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(
customer=create_billing_customer(has_active_subscription=True),
)
mock.json.return_value["customer"]["usage_summary"]["events"]["usage"] = 1000
elif "api/products" in url:
mock.status_code = 200
mock.json.return_value = create_billing_products_response()
return mock
mock_request.side_effect = mock_implementation
assert not self.organization.usage
res = self.client.get("/api/billing")
assert res.status_code == 200
self.organization.refresh_from_db()
assert self.organization.usage == {
"events": {
"limit": None,
"todays_usage": 0,
"usage": 1000,
},
"recordings": {
"limit": None,
"todays_usage": 0,
"usage": 0,
},
"rows_synced": {
"limit": None,
"todays_usage": 0,
"usage": 0,
},
"period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"],
}
self.organization.usage = {"events": {"limit": None, "usage": 1000, "todays_usage": 1100000}}
self.organization.save()
res = self.client.get("/api/billing")
assert res.status_code == 200
res_json = res.json()
# Should update product usage to reflect today's usage
assert res_json["products"][0]["current_usage"] == 1101000
assert res_json["products"][0]["current_amount_usd"] == "0.00"
assert res_json["products"][0]["tiers"][0]["current_amount_usd"] == "0.00"
assert res_json["products"][0]["tiers"][1]["current_amount_usd"] == "0.00"
assert res_json["products"][0]["addons"][0]["current_usage"] == 0
assert res_json["products"][0]["addons"][0]["current_amount_usd"] == "0.00"
assert res_json["products"][0]["addons"][0]["tiers"][0]["current_amount_usd"] == "0.00"
assert res_json["products"][0]["addons"][0]["tiers"][1]["current_amount_usd"] == "0.00"
@patch("ee.api.billing.requests.get")
def test_organization_usage_count_with_demo_project(self, mock_request, *args):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(
# Set usage to none so it is calculated from scratch
customer=create_billing_customer(has_active_subscription=False, usage=None)
)
return mock
mock_request.side_effect = mock_implementation
self.organization.customer_id = None
self.organization.usage = None
self.organization.save()
# Create a demo project
self.organization_membership.level = OrganizationMembership.Level.ADMIN
self.organization_membership.save()
self.assertEqual(Team.objects.count(), 1)
response = self.client.post("/api/projects/", {"name": "Test", "is_demo": True})
self.assertEqual(response.status_code, 201)
self.assertEqual(Team.objects.count(), 3)
demo_team = Team.objects.filter(is_demo=True).first()
# We create some events for the demo project
with self.settings(USE_TZ=False):
distinct_id = str(uuid4())
for _ in range(0, 10):
_create_event(
distinct_id=distinct_id,
event="$demo-event",
properties={"$lib": "$mobile"},
timestamp=now() - relativedelta(hours=12),
team=demo_team,
)
flush_persons_and_events()
assert not self.organization.usage
res = self.client.get("/api/billing")
assert res.status_code == 200
self.organization.refresh_from_db()
assert self.organization.usage == {
"events": {"limit": None, "usage": 0, "todays_usage": 0},
"recordings": {"limit": None, "usage": 0, "todays_usage": 0},
"rows_synced": {"limit": None, "usage": 0, "todays_usage": 0},
"period": ["2022-10-07T11:12:48", "2022-11-07T11:12:48"],
}
@patch("ee.api.billing.requests.get")
def test_org_trust_score_updated(self, mock_request):
def mock_implementation(url: str, headers: Any = None, params: Any = None) -> MagicMock:
mock = MagicMock()
mock.status_code = 404
if "api/billing/portal" in url:
mock.status_code = 200
mock.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
elif "api/billing" in url:
mock.status_code = 200
mock.json.return_value = create_billing_response(
# Set usage to none so it is calculated from scratch
customer=create_billing_customer(has_active_subscription=False, usage=None)
)
return mock
mock_request.side_effect = mock_implementation
self.organization.customer_id = None
self.organization.customer_trust_scores = {"recordings": 0, "events": 0, "rows_synced": 0}
self.organization.save()
res = self.client.get("/api/billing")
assert res.status_code == 200
self.organization.refresh_from_db()
assert self.organization.customer_trust_scores == {"recordings": 0, "events": 15, "rows_synced": 0}
class TestPortalBillingAPI(APILicensedTest):
@patch("ee.api.billing.requests.get")
def test_portal_success(self, mock_request):
mock_request.return_value.status_code = 200
mock_request.return_value.json.return_value = {"url": "https://billing.stripe.com/p/session/test_1234"}
response = self.client.get("/api/billing/portal")
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("https://billing.stripe.com/p/session/test_1234", response.url)
class TestActivateBillingAPI(APILicensedTest):
def test_activate_success(self):
url = "/api/billing/activate"
data = {"products": "product_1:plan_1,product_2:plan_2", "redirect_path": "custom/path"}
response = self.client.get(url, data=data)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("/activate", response.url)
self.assertIn("products=product_1:plan_1,product_2:plan_2", response.url)
url_pattern = r"redirect_uri=http://[^/]+/custom/path"
self.assertRegex(response.url, url_pattern)
def test_deprecated_activation_success(self):
url = "/api/billing/activate"
data = {"products": "product_1:plan_1,product_2:plan_2", "redirect_path": "custom/path"}
response = self.client.get(url, data=data)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("/activate", response.url)
self.assertIn("products=product_1:plan_1,product_2:plan_2", response.url)
url_pattern = r"redirect_uri=http://[^/]+/custom/path"
self.assertRegex(response.url, url_pattern)
def test_activate_with_default_redirect_path(self):
url = "/api/billing/activate"
data = {
"products": "product_1:plan_1,product_2:plan_2",
}
response = self.client.get(url, data)
self.assertEqual(response.status_code, status.HTTP_302_FOUND)
self.assertIn("products=product_1:plan_1,product_2:plan_2", response.url)
url_pattern = r"redirect_uri=http://[^/]+/organization/billing"
self.assertRegex(response.url, url_pattern)
def test_activate_failure(self):
url = "/api/billing/activate"
data = {"none": "nothing"}
response = self.client.get(url, data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
def test_activate_with_plan_error(self):
url = "/api/billing/activate"
data = {"plan": "plan"}
response = self.client.get(url, data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.json(),
{
"attr": "plan",
"code": "invalid_input",
"detail": "The 'plan' parameter is no longer supported. Please use the 'products' parameter instead.",
"type": "validation_error",
},
)
@patch("ee.billing.billing_manager.BillingManager.deactivate_products")
@patch("ee.billing.billing_manager.BillingManager.get_billing")
def test_deactivate_success(self, mock_get_billing, mock_deactivate_products):
mock_deactivate_products.return_value = MagicMock()
mock_get_billing.return_value = {
"available_features": [],
"products": [],
}
url = "/api/billing/deactivate"
data = {"products": "product_1"}
response = self.client.get(url, data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
mock_deactivate_products.assert_called_once_with(self.organization, "product_1")
mock_get_billing.assert_called_once_with(self.organization, None)
def test_deactivate_failure(self):
url = "/api/billing/deactivate"
data = {"none": "nothing"}
response = self.client.get(url, data)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)