diff --git a/sdks/implementations/python/tests/test_app.py b/sdks/implementations/python/tests/test_app.py index 716e50536..3c789bb75 100644 --- a/sdks/implementations/python/tests/test_app.py +++ b/sdks/implementations/python/tests/test_app.py @@ -2413,3 +2413,614 @@ class TestAsyncListConnectedAccounts: assert len(accounts) == 1 assert accounts[0].id == "provider-1" await app.aclose() + + +# =========================================================================== +# Payments +# =========================================================================== + +PRODUCT_JSON = { + "id": "prod-1", + "quantity": 5, + "displayName": "Premium Plan", + "customerType": "user", + "isServerOnly": False, + "stackable": False, + "type": "subscription", +} + +ITEM_JSON = { + "displayName": "Credits", + "quantity": 100, + "nonNegativeQuantity": 100, +} + + +# --------------------------------------------------------------------------- +# StackServerApp - list_products +# --------------------------------------------------------------------------- + + +class TestListProducts: + @respx.mock + def test_list_products_with_user_id(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/products").mock( + return_value=httpx.Response( + 200, + json={ + "items": [PRODUCT_JSON], + "pagination": {"next_cursor": "cur-1"}, + }, + ) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + result = app.list_products(user_id="user-1") + assert len(result.items) == 1 + assert result.items[0].display_name == "Premium Plan" + assert result.items[0].customer_type == "user" + assert result.next_cursor == "cur-1" + + @respx.mock + def test_list_products_with_team_id(self) -> None: + respx.get(f"{API_PREFIX}/customers/team/team-1/products").mock( + return_value=httpx.Response( + 200, json={"items": [], "pagination": {}} + ) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + result = app.list_products(team_id="team-1") + assert len(result.items) == 0 + + @respx.mock + def test_list_products_with_custom_customer_id(self) -> None: + respx.get(f"{API_PREFIX}/customers/custom/cust-1/products").mock( + return_value=httpx.Response( + 200, + json={"items": [PRODUCT_JSON], "pagination": {}}, + ) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + result = app.list_products(custom_customer_id="cust-1") + assert len(result.items) == 1 + + +# --------------------------------------------------------------------------- +# StackServerApp - get_item +# --------------------------------------------------------------------------- + + +class TestGetItem: + @respx.mock + def test_get_item_success(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + server_item = app.get_item("credits", user_id="user-1") + assert server_item.display_name == "Credits" + assert server_item.quantity == 100 + assert server_item.non_negative_quantity == 100 + + @respx.mock + def test_get_item_increase_quantity(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + qty_route = respx.post(f"{API_PREFIX}/internal/items/quantity-changes").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + server_item = app.get_item("credits", user_id="user-1") + server_item.increase_quantity(10) + import json as _json + + body = _json.loads(qty_route.calls[0].request.content) + assert body["quantity"] == 10 + assert body["item_id"] == "credits" + + @respx.mock + def test_get_item_decrease_quantity(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + qty_route = respx.post(f"{API_PREFIX}/internal/items/quantity-changes").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + server_item = app.get_item("credits", user_id="user-1") + server_item.decrease_quantity(5) + import json as _json + + body = _json.loads(qty_route.calls[0].request.content) + assert body["quantity"] == -5 + + @respx.mock + def test_get_item_try_decrease_quantity(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + respx.post(f"{API_PREFIX}/internal/items/try-decrease").mock( + return_value=httpx.Response(200, json={"success": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + server_item = app.get_item("credits", user_id="user-1") + assert server_item.try_decrease_quantity(5) is True + + @respx.mock + def test_get_item_try_decrease_quantity_fails(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + respx.post(f"{API_PREFIX}/internal/items/try-decrease").mock( + return_value=httpx.Response(200, json={"success": False}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + server_item = app.get_item("credits", user_id="user-1") + assert server_item.try_decrease_quantity(999) is False + + +# --------------------------------------------------------------------------- +# StackServerApp - grant_product +# --------------------------------------------------------------------------- + + +class TestGrantProduct: + @respx.mock + def test_grant_product_by_id(self) -> None: + route = respx.post(f"{API_PREFIX}/customers/user/user-1/products").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.grant_product(product_id="prod-1", user_id="user-1") + import json as _json + + body = _json.loads(route.calls[0].request.content) + assert body["product_id"] == "prod-1" + + @respx.mock + def test_grant_product_with_quantity(self) -> None: + route = respx.post(f"{API_PREFIX}/customers/team/team-1/products").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.grant_product(product_id="prod-1", team_id="team-1", quantity=3) + import json as _json + + body = _json.loads(route.calls[0].request.content) + assert body["product_id"] == "prod-1" + assert body["quantity"] == 3 + + +# --------------------------------------------------------------------------- +# StackServerApp - cancel_subscription +# --------------------------------------------------------------------------- + + +class TestCancelSubscription: + @respx.mock + def test_cancel_subscription(self) -> None: + route = respx.post(f"{API_PREFIX}/subscriptions/cancel").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.cancel_subscription("prod-1", user_id="user-1") + import json as _json + + body = _json.loads(route.calls[0].request.content) + assert body["product_id"] == "prod-1" + assert body["user_id"] == "user-1" + + @respx.mock + def test_cancel_subscription_with_team(self) -> None: + route = respx.post(f"{API_PREFIX}/subscriptions/cancel").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.cancel_subscription("prod-1", team_id="team-1") + import json as _json + + body = _json.loads(route.calls[0].request.content) + assert body["product_id"] == "prod-1" + assert body["team_id"] == "team-1" + + +# --------------------------------------------------------------------------- +# AsyncStackServerApp - payments +# --------------------------------------------------------------------------- + + +class TestAsyncListProducts: + @respx.mock + @pytest.mark.asyncio + async def test_list_products(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/products").mock( + return_value=httpx.Response( + 200, + json={"items": [PRODUCT_JSON], "pagination": {}}, + ) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + result = await app.list_products(user_id="user-1") + assert len(result.items) == 1 + assert result.items[0].display_name == "Premium Plan" + await app.aclose() + + +class TestAsyncGetItem: + @respx.mock + @pytest.mark.asyncio + async def test_get_item(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + server_item = await app.get_item("credits", user_id="user-1") + assert server_item.display_name == "Credits" + assert server_item.quantity == 100 + await app.aclose() + + @respx.mock + @pytest.mark.asyncio + async def test_get_item_increase_quantity(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + respx.post(f"{API_PREFIX}/internal/items/quantity-changes").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + server_item = await app.get_item("credits", user_id="user-1") + await server_item.increase_quantity(10) + await app.aclose() + + @respx.mock + @pytest.mark.asyncio + async def test_get_item_try_decrease(self) -> None: + respx.get(f"{API_PREFIX}/customers/user/user-1/items/credits").mock( + return_value=httpx.Response(200, json=ITEM_JSON) + ) + respx.post(f"{API_PREFIX}/internal/items/try-decrease").mock( + return_value=httpx.Response(200, json={"success": True}) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + server_item = await app.get_item("credits", user_id="user-1") + result = await server_item.try_decrease_quantity(5) + assert result is True + await app.aclose() + + +class TestAsyncGrantProduct: + @respx.mock + @pytest.mark.asyncio + async def test_grant_product(self) -> None: + respx.post(f"{API_PREFIX}/customers/user/user-1/products").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + await app.grant_product(product_id="prod-1", user_id="user-1") + await app.aclose() + + +class TestAsyncCancelSubscription: + @respx.mock + @pytest.mark.asyncio + async def test_cancel_subscription(self) -> None: + respx.post(f"{API_PREFIX}/subscriptions/cancel").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = AsyncStackServerApp(project_id="proj", secret_server_key="sk") + await app.cancel_subscription("prod-1", user_id="user-1") + await app.aclose() + + +# =========================================================================== +# Email +# =========================================================================== + + +# --------------------------------------------------------------------------- +# StackServerApp - send_email +# --------------------------------------------------------------------------- + + +class TestSendEmail: + @respx.mock + def test_send_email_with_html(self) -> None: + route = respx.post(f"{API_PREFIX}/emails").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.send_email("alice@example.com", "Hello", html="
Hello all
", + ) + import json as _json + + body = _json.loads(route.calls[0].request.content) + assert body["to"] == ["alice@example.com", "bob@example.com"] + + @respx.mock + def test_send_email_with_html_and_text(self) -> None: + route = respx.post(f"{API_PREFIX}/emails").mock( + return_value=httpx.Response(200, json={"ok": True}) + ) + app = StackServerApp(project_id="proj", secret_server_key="sk") + app.send_email( + "alice@example.com", + "Both", + html="