Reduce Supabase disk IO
This commit is contained in:
@@ -381,6 +381,46 @@ def test_join_request_approves_trial_user_with_queued_paid_subscription(monkeypa
|
||||
assert bot.approved_join_requests == [{"chat_id": -100123, "user_id": 12345}]
|
||||
|
||||
|
||||
def test_join_request_uses_subscription_window_without_latest_fallback(monkeypatch):
|
||||
monkeypatch.setenv("POLYWEATHER_TELEGRAM_GROUP_ID", "-100123")
|
||||
bot = DummyBot()
|
||||
db = SimpleNamespace(list_supabase_user_ids_for_telegram=lambda telegram_id: ["user-1"])
|
||||
io_layer = SimpleNamespace(
|
||||
build_welcome_text=lambda: "WELCOME",
|
||||
build_points_rank_text=lambda _user: "TOP",
|
||||
db=db,
|
||||
)
|
||||
|
||||
def _fail_latest(*_args, **_kwargs):
|
||||
raise AssertionError("subscription window rows should avoid latest subscription fallback")
|
||||
|
||||
entitlement = SimpleNamespace(
|
||||
get_subscription_window=lambda user_id, respect_requirement=False: {
|
||||
"rows": [
|
||||
{"plan_code": "signup_trial_3d", "source": "signup_trial"},
|
||||
]
|
||||
},
|
||||
get_latest_active_subscription=_fail_latest,
|
||||
)
|
||||
handler = BasicCommandHandler(
|
||||
bot=bot,
|
||||
io_layer=io_layer,
|
||||
runtime_status_provider=lambda: RuntimeStatus(
|
||||
started_at="2026-03-12 00:00:00 UTC",
|
||||
loops=[],
|
||||
command_access_mode="group_member",
|
||||
protected_commands=["/city", "/deb"],
|
||||
required_group_chat_id="-100123",
|
||||
),
|
||||
entitlement_service=entitlement,
|
||||
)
|
||||
|
||||
result = handler.handle_chat_join_request(_join_request())
|
||||
|
||||
assert result == "pending:no_active_subscription"
|
||||
assert bot.approved_join_requests == []
|
||||
|
||||
|
||||
def test_join_request_can_decline_ineligible_user_when_configured(monkeypatch):
|
||||
monkeypatch.setenv("POLYWEATHER_TELEGRAM_GROUP_ID", "-100123")
|
||||
monkeypatch.setenv("POLYWEATHER_TELEGRAM_JOIN_INELIGIBLE_ACTION", "decline")
|
||||
|
||||
@@ -186,14 +186,15 @@ def test_confirm_direct_transfer_uses_intent_chain_rpc(monkeypatch, tmp_path):
|
||||
)
|
||||
confirmed_intent = PaymentIntentRecord(**{**intent.__dict__, "status": "confirmed"})
|
||||
intents = [intent, confirmed_intent]
|
||||
get_intent_calls = []
|
||||
requested_chains = []
|
||||
tx_rows = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"get_intent",
|
||||
lambda user_id, intent_id: intents.pop(0) if intents else confirmed_intent,
|
||||
)
|
||||
def fake_get_intent(user_id, intent_id):
|
||||
get_intent_calls.append((user_id, intent_id))
|
||||
return intents.pop(0) if intents else confirmed_intent
|
||||
|
||||
monkeypatch.setattr(service, "get_intent", fake_get_intent)
|
||||
|
||||
class _Eth:
|
||||
chain_id = 1
|
||||
@@ -260,7 +261,9 @@ def test_confirm_direct_transfer_uses_intent_chain_rpc(monkeypatch, tmp_path):
|
||||
if method == "GET" and table == "payment_transactions":
|
||||
return []
|
||||
if method == "PATCH" and table == "payment_intents":
|
||||
return [{"id": intent.intent_id, "status": "confirmed"}]
|
||||
assert kwargs["prefer"] == "return=representation"
|
||||
assert kwargs["params"]["select"] == "id"
|
||||
return [{"id": intent.intent_id}]
|
||||
if method == "POST" and table == "payment_transactions":
|
||||
tx_rows.append(kwargs["payload"])
|
||||
return [kwargs["payload"]]
|
||||
@@ -273,6 +276,7 @@ def test_confirm_direct_transfer_uses_intent_chain_rpc(monkeypatch, tmp_path):
|
||||
assert 1 in requested_chains
|
||||
assert tx_rows[0]["chain_id"] == 1
|
||||
assert result["payment"]["chain_id"] == 1
|
||||
assert get_intent_calls == [("user-1", intent.intent_id)]
|
||||
|
||||
|
||||
def test_direct_intent_does_not_require_bound_wallet(monkeypatch, tmp_path):
|
||||
@@ -344,7 +348,7 @@ def test_direct_submit_tx_does_not_require_from_address(monkeypatch, tmp_path):
|
||||
monkeypatch.setattr(service, "_rest", fake_rest)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"validate_intent_tx",
|
||||
"_validate_loaded_intent_tx",
|
||||
lambda *args, **kwargs: {"valid": True, "checks": {"tx_mined": True}},
|
||||
)
|
||||
|
||||
@@ -384,7 +388,7 @@ def test_submit_rejects_direct_tx_until_validation_passes(monkeypatch, tmp_path)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"validate_intent_tx",
|
||||
"_validate_loaded_intent_tx",
|
||||
lambda *args, **kwargs: {
|
||||
"valid": False,
|
||||
"reason": "tx_not_mined",
|
||||
@@ -437,7 +441,7 @@ def test_submit_rejects_mined_direct_tx_when_receiver_mismatches(monkeypatch, tm
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"validate_intent_tx",
|
||||
"_validate_loaded_intent_tx",
|
||||
lambda *args, **kwargs: {
|
||||
"valid": False,
|
||||
"reason": "receiver_mismatch",
|
||||
@@ -543,7 +547,7 @@ def test_confirm_direct_transfer_uses_erc20_transfer_without_wallet_binding(monk
|
||||
if method == "PATCH" and table == "payment_intents":
|
||||
return [{"id": intent.intent_id, "status": "confirmed"}]
|
||||
if method == "POST" and table == "payment_transactions":
|
||||
return [kwargs["payload"]]
|
||||
return []
|
||||
raise AssertionError((method, table, kwargs))
|
||||
|
||||
monkeypatch.setattr(service, "_rest", fake_rest)
|
||||
@@ -552,8 +556,14 @@ def test_confirm_direct_transfer_uses_erc20_transfer_without_wallet_binding(monk
|
||||
result = service.confirm_intent_tx("user-1", intent.intent_id, tx_hash)
|
||||
|
||||
assert result["subscription"]["status"] == "active"
|
||||
assert result["transaction"]["tx_hash"] == tx_hash
|
||||
assert result["tx"]["event"]["amount_units"] == 5000000
|
||||
assert any(call[1] == "payment_transactions" for call in rest_calls)
|
||||
transaction_write = next(
|
||||
call
|
||||
for call in rest_calls
|
||||
if call[0] == "POST" and call[1] == "payment_transactions"
|
||||
)
|
||||
assert transaction_write[2]["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
|
||||
def test_submit_rejects_tx_hash_used_by_another_intent(monkeypatch, tmp_path):
|
||||
_setup_env(monkeypatch, tmp_path)
|
||||
|
||||
@@ -21,6 +21,517 @@ def _payment_env(monkeypatch, tmp_path):
|
||||
monkeypatch.delenv("POLYWEATHER_PAYMENT_DIRECT_RECEIVER_ADDRESS", raising=False)
|
||||
|
||||
|
||||
def test_wallet_challenge_insert_uses_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.create_wallet_challenge(
|
||||
"user-1",
|
||||
"0x1111111111111111111111111111111111111111",
|
||||
)
|
||||
|
||||
assert result["address"] == "0x1111111111111111111111111111111111111111"
|
||||
assert calls[0]["table"] == "wallet_link_challenges"
|
||||
assert calls[0]["prefer"] == "return=minimal"
|
||||
|
||||
|
||||
def test_entitlement_event_insert_uses_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "subscriptions":
|
||||
return []
|
||||
if method == "POST" and table == "subscriptions":
|
||||
return [kwargs.get("payload") or {}]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
service._grant_subscription(
|
||||
user_id="user-1",
|
||||
plan_code="pro_monthly",
|
||||
duration_days=30,
|
||||
tx_hash="0x" + "1" * 64,
|
||||
payload={"kind": "test"},
|
||||
)
|
||||
|
||||
entitlement_event = next(call for call in calls if call["table"] == "entitlement_events")
|
||||
assert entitlement_event["prefer"] == "return=minimal"
|
||||
|
||||
|
||||
def test_subscription_insert_uses_minimal_return_and_local_payload(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "subscriptions":
|
||||
return []
|
||||
if method == "POST" and table == "subscriptions":
|
||||
return []
|
||||
if method == "POST" and table == "entitlement_events":
|
||||
return []
|
||||
raise AssertionError((method, table, kwargs))
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service._grant_subscription(
|
||||
user_id="user-1",
|
||||
plan_code="pro_monthly",
|
||||
duration_days=30,
|
||||
tx_hash="0x" + "1" * 64,
|
||||
payload={"kind": "test"},
|
||||
)
|
||||
|
||||
subscription_write = next(
|
||||
call for call in calls if call["method"] == "POST" and call["table"] == "subscriptions"
|
||||
)
|
||||
assert subscription_write["prefer"] == "return=minimal"
|
||||
assert result == subscription_write["payload"]
|
||||
assert result["user_id"] == "user-1"
|
||||
assert result["plan_code"] == "pro_monthly"
|
||||
assert result["status"] == "active"
|
||||
|
||||
|
||||
def test_wallet_binding_writes_use_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
address = "0x1111111111111111111111111111111111111111"
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.payments.contract_checkout.Account.recover_message",
|
||||
lambda *args, **kwargs: address,
|
||||
)
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "wallet_link_challenges":
|
||||
return [
|
||||
{
|
||||
"id": "challenge-1",
|
||||
"user_id": "user-1",
|
||||
"address": address,
|
||||
"nonce": "nonce-1",
|
||||
"message": "message",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"consumed_at": None,
|
||||
}
|
||||
]
|
||||
if method == "GET" and table == "user_wallets":
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.verify_wallet_binding("user-1", address, "nonce-1", "0xsig")
|
||||
|
||||
assert result.address == address
|
||||
writes = [call for call in calls if call["method"] in {"POST", "PATCH"}]
|
||||
assert writes[0]["table"] == "user_wallets"
|
||||
assert writes[0]["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
assert writes[1]["table"] == "wallet_link_challenges"
|
||||
assert writes[1]["prefer"] == "return=minimal"
|
||||
|
||||
|
||||
def test_require_user_wallet_selects_only_status(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [{"status": "active"}]
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service._require_user_wallet(
|
||||
"user-1",
|
||||
"0x1111111111111111111111111111111111111111",
|
||||
)
|
||||
|
||||
assert result == {"status": "active"}
|
||||
assert calls[0]["params"]["select"] == "status"
|
||||
|
||||
|
||||
def test_list_wallets_omits_status_from_active_wallet_query(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [
|
||||
{
|
||||
"chain_id": 137,
|
||||
"address": "0x1111111111111111111111111111111111111111",
|
||||
"is_primary": True,
|
||||
"verified_at": "2099-01-01T00:00:00+00:00",
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
wallets = service.list_wallets("user-1")
|
||||
|
||||
assert calls[0]["params"]["select"] == "chain_id,address,is_primary,verified_at"
|
||||
assert wallets[0].status == "active"
|
||||
|
||||
|
||||
def test_wallet_binding_existing_lookup_selects_only_owner_status(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
address = "0x1111111111111111111111111111111111111111"
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.payments.contract_checkout.Account.recover_message",
|
||||
lambda *args, **kwargs: address,
|
||||
)
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "wallet_link_challenges":
|
||||
return [
|
||||
{
|
||||
"id": "challenge-1",
|
||||
"user_id": "user-1",
|
||||
"address": address,
|
||||
"nonce": "nonce-1",
|
||||
"message": "message",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"consumed_at": None,
|
||||
}
|
||||
]
|
||||
if method == "GET" and table == "user_wallets":
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
service.verify_wallet_binding("user-1", address, "nonce-1", "0xsig")
|
||||
|
||||
existing_lookup = [
|
||||
call
|
||||
for call in calls
|
||||
if call["method"] == "GET"
|
||||
and call["table"] == "user_wallets"
|
||||
and "address" in call["params"]
|
||||
][0]
|
||||
assert existing_lookup["params"]["select"] == "user_id,status"
|
||||
|
||||
challenge_lookup = [
|
||||
call
|
||||
for call in calls
|
||||
if call["method"] == "GET"
|
||||
and call["table"] == "wallet_link_challenges"
|
||||
][0]
|
||||
assert challenge_lookup["params"]["select"] == "id,message,expires_at"
|
||||
assert "order" not in challenge_lookup["params"]
|
||||
|
||||
|
||||
def test_wallet_unbind_writes_use_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(service, "_require_user_wallet", lambda *args, **kwargs: {})
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "user_wallets" and kwargs["params"].get("is_primary") == "eq.true":
|
||||
return []
|
||||
if method == "GET" and table == "user_wallets":
|
||||
return [{"id": "wallet-2", "address": "0x2222222222222222222222222222222222222222"}]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.unbind_wallet("user-1", "0x1111111111111111111111111111111111111111")
|
||||
|
||||
assert result["new_primary"] == "0x2222222222222222222222222222222222222222"
|
||||
writes = [call for call in calls if call["method"] == "PATCH"]
|
||||
assert [call["prefer"] for call in writes] == ["return=minimal", "return=minimal"]
|
||||
|
||||
|
||||
def test_submit_intent_status_patch_uses_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"get_intent",
|
||||
lambda user_id, intent_id: service._serialize_intent(
|
||||
{
|
||||
"id": intent_id,
|
||||
"plan_code": "pro_monthly",
|
||||
"plan_id": 101,
|
||||
"chain_id": 137,
|
||||
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"receiver_address": "0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32",
|
||||
"amount_units": "5000000",
|
||||
"payment_mode": "direct",
|
||||
"allowed_wallet": None,
|
||||
"order_id_hex": "0x" + "1" * 64,
|
||||
"status": "created",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"tx_hash": None,
|
||||
"metadata": {},
|
||||
}
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_validate_loaded_intent_tx",
|
||||
lambda *args, **kwargs: {"valid": True, "checks": {"tx_mined": True}},
|
||||
)
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "payment_transactions":
|
||||
return []
|
||||
if method == "POST" and table == "payment_transactions":
|
||||
return []
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.submit_intent_tx("user-1", "intent-1", "0x" + "2" * 64, "")
|
||||
|
||||
assert result["status"] == "submitted"
|
||||
intent_patch = next(call for call in calls if call["method"] == "PATCH")
|
||||
assert intent_patch["table"] == "payment_intents"
|
||||
assert intent_patch["prefer"] == "return=minimal"
|
||||
transaction_write = next(
|
||||
call
|
||||
for call in calls
|
||||
if call["method"] == "POST" and call["table"] == "payment_transactions"
|
||||
)
|
||||
assert transaction_write["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
assert result["transaction"]["tx_hash"] == "0x" + "2" * 64
|
||||
|
||||
|
||||
def test_submit_intent_tx_reuses_loaded_intent_for_validation(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
intent = service._serialize_intent(
|
||||
{
|
||||
"id": "intent-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"plan_id": 101,
|
||||
"chain_id": 137,
|
||||
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"receiver_address": "0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32",
|
||||
"amount_units": "5000000",
|
||||
"payment_mode": "direct",
|
||||
"allowed_wallet": None,
|
||||
"order_id_hex": "0x" + "1" * 64,
|
||||
"status": "created",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"tx_hash": None,
|
||||
"metadata": {},
|
||||
}
|
||||
)
|
||||
calls = {"get_intent": 0}
|
||||
rest_calls = []
|
||||
|
||||
class _FakeEth:
|
||||
def get_transaction_receipt(self, tx_hash):
|
||||
return {"status": 1, "to": intent.receiver_address, "blockNumber": 123}
|
||||
|
||||
class _FakeWeb3:
|
||||
eth = _FakeEth()
|
||||
|
||||
def _fake_get_intent(user_id, intent_id):
|
||||
calls["get_intent"] += 1
|
||||
return intent
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
rest_calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "GET" and table == "payment_transactions":
|
||||
return []
|
||||
if method == "POST" and table == "payment_transactions":
|
||||
return [kwargs["payload"]]
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "get_intent", _fake_get_intent)
|
||||
monkeypatch.setattr(service, "_get_web3", lambda *args, **kwargs: _FakeWeb3())
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_extract_direct_transfer_event",
|
||||
lambda receipt, loaded_intent: {
|
||||
"from": "0x2222222222222222222222222222222222222222",
|
||||
"to": loaded_intent.receiver_address,
|
||||
"amount_units": int(loaded_intent.amount_units),
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.submit_intent_tx("user-1", "intent-1", "0x" + "2" * 64, "")
|
||||
|
||||
assert result["status"] == "submitted"
|
||||
assert calls["get_intent"] == 1
|
||||
assert [call["table"] for call in rest_calls].count("payment_transactions") == 2
|
||||
|
||||
|
||||
def test_failed_intent_writes_use_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
intent = PaymentIntentRecord(
|
||||
intent_id="intent-1",
|
||||
order_id_hex="0x" + "1" * 64,
|
||||
plan_code="pro_monthly",
|
||||
plan_id=101,
|
||||
chain_id=137,
|
||||
amount_units=5_000_000,
|
||||
amount_usdc="5",
|
||||
token_address="0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
|
||||
token_decimals=6,
|
||||
token_symbol="USDC",
|
||||
receiver_address="0xed2f13aa5ff033c58fb436e178451cd07f693f32",
|
||||
status="submitted",
|
||||
payment_mode="direct",
|
||||
allowed_wallet=None,
|
||||
expires_at="2099-01-01T00:00:00+00:00",
|
||||
tx_hash=None,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
service._mark_intent_failed(
|
||||
user_id="user-1",
|
||||
intent=intent,
|
||||
tx_hash="0x" + "3" * 64,
|
||||
reason="test_failure",
|
||||
detail="test",
|
||||
)
|
||||
|
||||
assert calls[0]["table"] == "payment_intents"
|
||||
assert calls[0]["prefer"] == "return=minimal"
|
||||
assert calls[1]["table"] == "payment_transactions"
|
||||
assert calls[1]["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
|
||||
|
||||
def test_duplicate_transaction_write_uses_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
intent = PaymentIntentRecord(
|
||||
intent_id="intent-1",
|
||||
order_id_hex="0x" + "1" * 64,
|
||||
plan_code="pro_monthly",
|
||||
plan_id=101,
|
||||
chain_id=137,
|
||||
amount_units=5_000_000,
|
||||
amount_usdc="5",
|
||||
token_address="0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
|
||||
token_decimals=6,
|
||||
token_symbol="USDC",
|
||||
receiver_address="0xed2f13aa5ff033c58fb436e178451cd07f693f32",
|
||||
status="confirmed",
|
||||
payment_mode="direct",
|
||||
allowed_wallet=None,
|
||||
expires_at="2099-01-01T00:00:00+00:00",
|
||||
tx_hash="0x" + "2" * 64,
|
||||
metadata={},
|
||||
)
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service._record_duplicate_transaction(
|
||||
intent=intent,
|
||||
tx_hash="0x" + "3" * 64,
|
||||
from_address="0x2222222222222222222222222222222222222222",
|
||||
status="refund_required",
|
||||
)
|
||||
|
||||
assert result == {}
|
||||
assert calls[0]["table"] == "payment_transactions"
|
||||
assert calls[0]["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
|
||||
|
||||
def test_payment_record_upsert_uses_minimal_return(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return []
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service._insert_payment_record(
|
||||
user_id="user-1",
|
||||
tx_hash="0x" + "4" * 64,
|
||||
amount_units=5_000_000,
|
||||
token_address="0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
|
||||
chain_id=137,
|
||||
payload={"kind": "test"},
|
||||
)
|
||||
|
||||
assert calls[0]["table"] == "payments"
|
||||
assert calls[0]["prefer"] == "resolution=merge-duplicates,return=minimal"
|
||||
assert result["tx_hash"] == "0x" + "4" * 64
|
||||
assert result["status"] == "confirmed"
|
||||
assert result["amount"] == "5"
|
||||
|
||||
|
||||
def test_create_intent_insert_uses_minimal_return_and_local_payload(
|
||||
monkeypatch, tmp_path
|
||||
):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
if method == "POST" and table == "payment_intents":
|
||||
return []
|
||||
raise AssertionError((method, table, kwargs))
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.create_intent(
|
||||
"00000000-0000-0000-0000-000000000001",
|
||||
"pro_monthly",
|
||||
payment_mode="direct",
|
||||
)
|
||||
|
||||
intent_write = calls[0]
|
||||
payload = intent_write["payload"]
|
||||
assert intent_write["prefer"] == "return=minimal"
|
||||
assert payload["id"] == result["intent"]["intent_id"]
|
||||
assert payload["order_id_hex"] == result["intent"]["order_id_hex"]
|
||||
assert result["intent"]["status"] == "created"
|
||||
assert result["direct_payment"]["amount_units"] == str(payload["amount_units"])
|
||||
|
||||
|
||||
def test_payment_runtime_state_and_audit_event_roundtrip(tmp_path):
|
||||
db_path = tmp_path / "payments.db"
|
||||
db = DBManager(str(db_path))
|
||||
@@ -246,6 +757,121 @@ def test_reconcile_latest_intent_confirms_submitted_first(monkeypatch, tmp_path)
|
||||
assert result["action"] == "confirmed_submitted_intent"
|
||||
|
||||
|
||||
def test_reconcile_latest_intent_reuses_confirmed_row_after_repair(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true")
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_RPC_URL", "https://rpc-1.example")
|
||||
monkeypatch.setenv(
|
||||
"POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON",
|
||||
'[{"code":"usdc_e","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32","is_default":true}]',
|
||||
)
|
||||
monkeypatch.setenv("POLYWEATHER_DB_PATH", str(tmp_path / "payments.db"))
|
||||
|
||||
service = PaymentContractCheckoutService()
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_rest",
|
||||
lambda method, table, **kwargs: [
|
||||
{
|
||||
"id": "intent-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"plan_id": 101,
|
||||
"chain_id": 137,
|
||||
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"receiver_address": "0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32",
|
||||
"amount_units": "5000000",
|
||||
"payment_mode": "strict",
|
||||
"allowed_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"order_id_hex": "0x" + "1" * 64,
|
||||
"status": "confirmed",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"tx_hash": "0x" + "2" * 64,
|
||||
"metadata": {},
|
||||
}
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"get_intent",
|
||||
lambda user_id, intent_id: (_ for _ in ()).throw(
|
||||
AssertionError("confirmed repair should reuse loaded row")
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_ensure_confirm_side_effects",
|
||||
lambda user_id, local_intent, tx_hash: {
|
||||
"payment": {"tx_hash": tx_hash},
|
||||
"subscription": {"plan_code": local_intent.plan_code},
|
||||
},
|
||||
)
|
||||
|
||||
result = service.reconcile_latest_intent("user-1")
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["action"] == "reconciled_confirmed_intent"
|
||||
assert result["intent"]["intent_id"] == "intent-1"
|
||||
|
||||
|
||||
def test_reconcile_latest_intent_without_candidates_does_not_clear_subscription_cache(
|
||||
monkeypatch,
|
||||
tmp_path,
|
||||
):
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true")
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_RPC_URL", "https://rpc-1.example")
|
||||
monkeypatch.setenv(
|
||||
"POLYWEATHER_PAYMENT_ACCEPTED_TOKENS_JSON",
|
||||
'[{"code":"usdc_e","address":"0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174","decimals":6,"receiver_contract":"0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32","is_default":true}]',
|
||||
)
|
||||
monkeypatch.setenv("POLYWEATHER_DB_PATH", str(tmp_path / "payments.db"))
|
||||
|
||||
service = PaymentContractCheckoutService()
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"_rest",
|
||||
lambda method, table, **kwargs: [
|
||||
{
|
||||
"id": "intent-created",
|
||||
"plan_code": "pro_monthly",
|
||||
"plan_id": 101,
|
||||
"chain_id": 137,
|
||||
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"receiver_address": "0xeD2f13Aa5fF033c58FB436E178451Cd07f693f32",
|
||||
"amount_units": "5000000",
|
||||
"payment_mode": "strict",
|
||||
"allowed_wallet": "0x1111111111111111111111111111111111111111",
|
||||
"order_id_hex": "0x" + "1" * 64,
|
||||
"status": "created",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"tx_hash": None,
|
||||
"metadata": {},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
invalidations = []
|
||||
monkeypatch.setattr(
|
||||
"src.payments.contract_checkout.SUPABASE_ENTITLEMENT.invalidate_subscription_cache",
|
||||
lambda user_id: invalidations.append(user_id),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.payments.contract_checkout.SUPABASE_ENTITLEMENT.get_latest_active_subscription",
|
||||
lambda user_id, respect_requirement=False: {
|
||||
"plan_code": "pro_monthly",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
},
|
||||
)
|
||||
|
||||
result = service.reconcile_latest_intent("user-1")
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["action"] == "checked_without_repair"
|
||||
assert invalidations == []
|
||||
|
||||
|
||||
def test_confirm_intent_tx_repairs_side_effect_failure(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true")
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
@@ -386,6 +1012,117 @@ def test_reconcile_recent_intents_dedupes_users(monkeypatch, tmp_path):
|
||||
assert seen == ["user-1", "user-2"]
|
||||
|
||||
|
||||
def test_reconcile_recent_intents_selects_only_user_ids(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [{"user_id": "user-1"}, {"user_id": "user-1"}, {"user_id": "user-2"}]
|
||||
|
||||
seen = []
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
monkeypatch.setattr(
|
||||
service,
|
||||
"reconcile_latest_intent",
|
||||
lambda user_id: seen.append(user_id) or {"ok": True, "subscription": {"user_id": user_id}},
|
||||
)
|
||||
|
||||
result = service.reconcile_recent_intents(limit=10)
|
||||
|
||||
assert calls[0]["params"]["select"] == "user_id"
|
||||
assert seen == ["user-1", "user-2"]
|
||||
assert result["processed_users"] == 2
|
||||
|
||||
|
||||
def test_pending_confirm_intents_selects_only_confirm_loop_fields(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [
|
||||
{
|
||||
"id": "intent-1",
|
||||
"user_id": "user-1",
|
||||
"chain_id": 137,
|
||||
"tx_hash": "0x" + "2" * 64,
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.list_pending_confirm_intents(limit=20)
|
||||
|
||||
assert result == [
|
||||
{
|
||||
"intent_id": "intent-1",
|
||||
"user_id": "user-1",
|
||||
"chain_id": 137,
|
||||
"tx_hash": "0x" + "2" * 64,
|
||||
}
|
||||
]
|
||||
assert calls[0]["params"]["select"] == "id,user_id,tx_hash,chain_id"
|
||||
|
||||
|
||||
def test_open_intents_by_order_id_selects_only_event_loop_fields(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [
|
||||
{
|
||||
"id": "intent-1",
|
||||
"user_id": "user-1",
|
||||
"status": "submitted",
|
||||
"tx_hash": "0x" + "2" * 64,
|
||||
"plan_id": 101,
|
||||
"token_address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
|
||||
"amount_units": "5000000",
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
result = service.list_open_intents_by_order_id("0x" + "1" * 64)
|
||||
|
||||
assert result == [
|
||||
{
|
||||
"intent_id": "intent-1",
|
||||
"user_id": "user-1",
|
||||
"status": "submitted",
|
||||
"tx_hash": "0x" + "2" * 64,
|
||||
"plan_id": 101,
|
||||
"token_address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
|
||||
"amount_units": 5000000,
|
||||
}
|
||||
]
|
||||
assert (
|
||||
calls[0]["params"]["select"]
|
||||
== "id,user_id,status,tx_hash,plan_id,token_address,amount_units"
|
||||
)
|
||||
|
||||
|
||||
def test_tx_hash_unused_check_selects_only_intent_id(monkeypatch, tmp_path):
|
||||
_payment_env(monkeypatch, tmp_path)
|
||||
service = PaymentContractCheckoutService()
|
||||
calls = []
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
calls.append({"method": method, "table": table, **kwargs})
|
||||
return [{"intent_id": "intent-1"}]
|
||||
|
||||
monkeypatch.setattr(service, "_rest", _fake_rest)
|
||||
|
||||
service._ensure_tx_hash_unused("0x" + "1" * 64, "intent-1")
|
||||
|
||||
assert calls[0]["params"]["select"] == "intent_id"
|
||||
|
||||
|
||||
def test_grant_subscription_starts_after_trial_when_only_trial_is_active(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("POLYWEATHER_PAYMENT_ENABLED", "true")
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
@@ -403,12 +1140,9 @@ def test_grant_subscription_starts_after_trial_when_only_trial_is_active(monkeyp
|
||||
|
||||
def _fake_rest(method, table, **kwargs):
|
||||
if method == "GET" and table == "subscriptions":
|
||||
assert kwargs["params"]["select"] == "starts_at,expires_at"
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"status": "active",
|
||||
"plan_code": "signup_trial_3d",
|
||||
"source": "signup_trial",
|
||||
"starts_at": (datetime.now(timezone.utc) - timedelta(days=1)).isoformat(),
|
||||
"expires_at": trial_end.isoformat(),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_script():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
path = root / "scripts" / "reconcile_payment_tx.py"
|
||||
spec = importlib.util.spec_from_file_location("reconcile_payment_tx", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_find_intent_by_tx_queries_tx_hash_directly(monkeypatch):
|
||||
module = _load_script()
|
||||
tx_hash = "0x" + "a" * 64
|
||||
calls = []
|
||||
|
||||
class FakeCheckout:
|
||||
def _rest(self, method, table, *, params, allowed_status):
|
||||
calls.append(
|
||||
{
|
||||
"method": method,
|
||||
"table": table,
|
||||
"params": params,
|
||||
"allowed_status": allowed_status,
|
||||
}
|
||||
)
|
||||
return [{"id": "intent-1", "user_id": "user-1", "tx_hash": tx_hash}]
|
||||
|
||||
monkeypatch.setattr(module, "PAYMENT_CHECKOUT", FakeCheckout())
|
||||
|
||||
result = module._find_intent_by_tx("user-1", tx_hash.upper())
|
||||
|
||||
assert result["id"] == "intent-1"
|
||||
assert calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"table": "payment_intents",
|
||||
"params": {
|
||||
"select": "id,user_id",
|
||||
"tx_hash": f"eq.{tx_hash}",
|
||||
"limit": "1",
|
||||
},
|
||||
"allowed_status": [200],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_find_intent_by_tx_rejects_other_user(monkeypatch):
|
||||
module = _load_script()
|
||||
|
||||
class FakeCheckout:
|
||||
def _rest(self, method, table, *, params, allowed_status):
|
||||
return [{"id": "intent-other", "user_id": "user-2"}]
|
||||
|
||||
monkeypatch.setattr(module, "PAYMENT_CHECKOUT", FakeCheckout())
|
||||
|
||||
assert module._find_intent_by_tx("user-1", "0x" + "a" * 64) is None
|
||||
|
||||
|
||||
def test_find_intent_by_order_id_uses_unique_order_lookup(monkeypatch):
|
||||
module = _load_script()
|
||||
calls = []
|
||||
|
||||
class FakeCheckout:
|
||||
def _rest(self, method, table, *, params, allowed_status):
|
||||
calls.append(
|
||||
{
|
||||
"method": method,
|
||||
"table": table,
|
||||
"params": params,
|
||||
"allowed_status": allowed_status,
|
||||
}
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": "intent-1",
|
||||
"user_id": "user-1",
|
||||
"order_id_hex": "0x" + "b" * 64,
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(module, "PAYMENT_CHECKOUT", FakeCheckout())
|
||||
|
||||
result = module._find_intent_by_order_id("user-1", "0X" + "B" * 64)
|
||||
|
||||
assert result["id"] == "intent-1"
|
||||
assert calls == [
|
||||
{
|
||||
"method": "GET",
|
||||
"table": "payment_intents",
|
||||
"params": {
|
||||
"select": "id,user_id",
|
||||
"order_id_hex": "eq.0x" + "b" * 64,
|
||||
"limit": "1",
|
||||
},
|
||||
"allowed_status": [200],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_find_intent_by_order_id_rejects_other_user(monkeypatch):
|
||||
module = _load_script()
|
||||
|
||||
class FakeCheckout:
|
||||
def _rest(self, method, table, *, params, allowed_status):
|
||||
return [
|
||||
{
|
||||
"id": "intent-other",
|
||||
"user_id": "user-2",
|
||||
"order_id_hex": "0x" + "b" * 64,
|
||||
}
|
||||
]
|
||||
|
||||
monkeypatch.setattr(module, "PAYMENT_CHECKOUT", FakeCheckout())
|
||||
|
||||
assert module._find_intent_by_order_id("user-1", "0x" + "b" * 64) is None
|
||||
@@ -0,0 +1,104 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _load_script(name: str):
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
path = root / "scripts" / f"{name}.py"
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec and spec.loader
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
grant_script = _load_script("grant_subscription_by_email")
|
||||
reconcile_script = _load_script("reconcile_subscription_by_email")
|
||||
|
||||
|
||||
class _Checkout:
|
||||
supabase_url = "https://example.supabase.co"
|
||||
supabase_service_role_key = "service-role"
|
||||
|
||||
def __init__(self):
|
||||
self.rest_calls = []
|
||||
self.admin_calls = []
|
||||
|
||||
def _rest(self, method, table, params=None, **kwargs):
|
||||
self.rest_calls.append((method, table, params))
|
||||
assert table == "profiles"
|
||||
return [{"id": "user-1", "email": "user@example.com"}]
|
||||
|
||||
def _auth_admin_request(self, *args, **kwargs):
|
||||
self.admin_calls.append((args, kwargs))
|
||||
raise AssertionError("profile-backed lookup should avoid Auth Admin")
|
||||
|
||||
|
||||
def test_grant_script_email_lookup_prefers_profiles(monkeypatch):
|
||||
checkout = _Checkout()
|
||||
monkeypatch.setattr("src.payments.contract_checkout.PAYMENT_CHECKOUT", checkout)
|
||||
|
||||
assert grant_script._lookup_user_id_by_email("user@example.com") == "user-1"
|
||||
assert checkout.rest_calls == [
|
||||
(
|
||||
"GET",
|
||||
"profiles",
|
||||
{
|
||||
"select": "id",
|
||||
"email": "eq.user@example.com",
|
||||
"limit": "1",
|
||||
},
|
||||
)
|
||||
]
|
||||
assert checkout.admin_calls == []
|
||||
|
||||
|
||||
def test_reconcile_script_email_lookup_prefers_profiles(monkeypatch):
|
||||
checkout = _Checkout()
|
||||
monkeypatch.setattr("src.payments.contract_checkout.PAYMENT_CHECKOUT", checkout)
|
||||
|
||||
assert reconcile_script._lookup_user_id_by_email("user@example.com") == "user-1"
|
||||
assert checkout.rest_calls == [
|
||||
(
|
||||
"GET",
|
||||
"profiles",
|
||||
{
|
||||
"select": "id",
|
||||
"email": "eq.user@example.com",
|
||||
"limit": "1",
|
||||
},
|
||||
)
|
||||
]
|
||||
assert checkout.admin_calls == []
|
||||
|
||||
|
||||
def test_grant_script_entitlement_event_uses_minimal_return():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / "scripts" / "grant_subscription_by_email.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
entitlement_event_pos = source.index('"entitlement_events"')
|
||||
minimal_pos = source.index('prefer="return=minimal"', entitlement_event_pos)
|
||||
assert minimal_pos > entitlement_event_pos
|
||||
|
||||
|
||||
def test_grant_script_subscription_writes_use_minimal_return():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / "scripts" / "grant_subscription_by_email.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
subscription_write_count = source.count('prefer="return=minimal"')
|
||||
assert subscription_write_count >= 3
|
||||
assert 'prefer="return=representation"' not in source
|
||||
|
||||
|
||||
def test_grant_script_subscription_lookup_selects_only_grant_fields():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
source = (root / "scripts" / "grant_subscription_by_email.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
assert '"select": "id,plan_code,source,starts_at,expires_at"' in source
|
||||
assert '"select": "id,expires_at,status,plan_code,starts_at,source,created_at"' not in source
|
||||
@@ -30,17 +30,11 @@ def test_latest_active_subscription_ignores_future_start(monkeypatch):
|
||||
"starts_at": (now - timedelta(days=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=2)).isoformat(),
|
||||
}
|
||||
future_paid = {
|
||||
"id": 2,
|
||||
"user_id": "user-1",
|
||||
"status": "active",
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": (now + timedelta(days=2)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=32)).isoformat(),
|
||||
}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
return _Response(200, [future_paid, current_trial])
|
||||
assert params["select"] == "plan_code,source,starts_at,expires_at"
|
||||
assert str(params["starts_at"]).startswith("lte.")
|
||||
assert params["limit"] == "1"
|
||||
return _Response(200, [current_trial])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
@@ -50,6 +44,44 @@ def test_latest_active_subscription_ignores_future_start(monkeypatch):
|
||||
assert result["plan_code"] == "signup_trial_3d"
|
||||
|
||||
|
||||
def test_get_identity_caches_invalid_token_result(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
return _Response(401, {"message": "invalid token"})
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.get_identity("bad-token") is None
|
||||
assert service.get_identity("bad-token") is None
|
||||
assert calls["count"] == 1
|
||||
|
||||
|
||||
def test_get_identity_does_not_cache_transient_auth_errors(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
return _Response(503, {"message": "temporarily unavailable"})
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.get_identity("temporarily-bad-token") is None
|
||||
assert service.get_identity("temporarily-bad-token") is None
|
||||
assert calls["count"] == 2
|
||||
|
||||
|
||||
def test_subscription_window_keeps_queued_renewal_after_current_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
@@ -75,7 +107,16 @@ def test_subscription_window_keeps_queued_renewal_after_current_cache(monkeypatc
|
||||
"expires_at": (now + timedelta(days=31)).isoformat(),
|
||||
}
|
||||
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(params)
|
||||
assert params["select"] == "plan_code,source,starts_at,expires_at"
|
||||
if params["limit"] == "1":
|
||||
assert str(params["starts_at"]).startswith("lte.")
|
||||
return _Response(200, [current])
|
||||
assert params["limit"] == "100"
|
||||
assert "starts_at" not in params
|
||||
return _Response(200, [queued, current])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
@@ -88,3 +129,412 @@ def test_subscription_window_keeps_queued_renewal_after_current_cache(monkeypatc
|
||||
assert window["total_expires_at"] == queued["expires_at"]
|
||||
assert window["queued_days"] == 30
|
||||
assert window["queued_count"] == 1
|
||||
assert len(calls) == 2
|
||||
|
||||
|
||||
def test_subscription_window_query_selects_only_window_fields(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
assert params["select"] == "plan_code,source,starts_at,expires_at"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
window = service.get_subscription_window(
|
||||
"user-1",
|
||||
respect_requirement=False,
|
||||
bypass_cache=True,
|
||||
)
|
||||
|
||||
assert window["current"]["plan_code"] == "pro_monthly"
|
||||
|
||||
|
||||
def test_list_subscription_windows_selects_only_batch_window_fields(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
assert params["select"] == "user_id,plan_code,source,starts_at,expires_at"
|
||||
assert params["user_id"] == "in.(user-1,user-2)"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"user_id": "user-2",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-02T00:00:00+00:00",
|
||||
"expires_at": "2099-04-02T00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
windows = service.list_subscription_windows(
|
||||
["user-1", "user-2"],
|
||||
bypass_cache=True,
|
||||
)
|
||||
|
||||
assert set(windows) == {"user-1", "user-2"}
|
||||
|
||||
|
||||
def test_list_active_subscription_windows_uses_single_window_query(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = []
|
||||
now = datetime.now(timezone.utc)
|
||||
current = {
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": (now - timedelta(days=1)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=10)).isoformat(),
|
||||
}
|
||||
queued = {
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": (now + timedelta(days=10)).isoformat(),
|
||||
"expires_at": (now + timedelta(days=40)).isoformat(),
|
||||
}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(params)
|
||||
assert params["select"] == "user_id,plan_code,source,starts_at,expires_at"
|
||||
assert params["status"] == "eq.active"
|
||||
assert params["order"] == "user_id.asc,expires_at.desc"
|
||||
return _Response(200, [queued, current])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
result = service.list_active_subscription_windows(limit=200)
|
||||
|
||||
assert result["subscriptions"] == [current]
|
||||
assert result["windows"]["user-1"]["queued_count"] == 1
|
||||
assert calls and len(calls) == 1
|
||||
|
||||
|
||||
def test_latest_subscription_any_status_uses_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
latest = {
|
||||
"id": 3,
|
||||
"user_id": "user-1",
|
||||
"status": "expired",
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
"updated_at": "2026-04-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
assert params["user_id"] == "eq.user-1"
|
||||
assert params["order"] == "created_at.desc"
|
||||
assert params["select"] == "plan_code,starts_at,expires_at"
|
||||
return _Response(200, [latest])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.get_latest_subscription_any_status("user-1") == latest
|
||||
assert service.get_latest_subscription_any_status("user-1") == latest
|
||||
assert calls["count"] == 1
|
||||
|
||||
|
||||
def test_get_auth_users_batches_profiles_before_admin_fallback(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append((url, params))
|
||||
if url.endswith("/rest/v1/profiles"):
|
||||
assert params["id"] == "in.(user-1,user-2)"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"id": "user-1",
|
||||
"email": "one@example.com",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"id": "user-2",
|
||||
"email": "two@example.com",
|
||||
"created_at": "2026-03-02T00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
)
|
||||
raise AssertionError(f"unexpected admin fallback call: {url}")
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
result = service.get_auth_users(["user-1", "user-2"])
|
||||
|
||||
assert result == {
|
||||
"user-1": {
|
||||
"email": "one@example.com",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
},
|
||||
"user-2": {
|
||||
"email": "two@example.com",
|
||||
"created_at": "2026-03-02T00:00:00+00:00",
|
||||
},
|
||||
}
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_get_auth_users_uses_short_cache_for_profile_results(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
assert url.endswith("/rest/v1/profiles")
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"id": "user-1",
|
||||
"email": "one@example.com",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.get_auth_users(["user-1"]) == {
|
||||
"user-1": {
|
||||
"email": "one@example.com",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
},
|
||||
}
|
||||
assert service.get_auth_users(["user-1"]) == {
|
||||
"user-1": {
|
||||
"email": "one@example.com",
|
||||
"created_at": "2026-03-01T00:00:00+00:00",
|
||||
},
|
||||
}
|
||||
assert calls["count"] == 1
|
||||
|
||||
|
||||
def test_list_active_subscriptions_uses_cache_and_invalidation(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
row = {
|
||||
"id": 1,
|
||||
"user_id": "user-1",
|
||||
"status": "active",
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
assert params["status"] == "eq.active"
|
||||
assert params["select"] == "user_id,plan_code,starts_at,expires_at"
|
||||
return _Response(200, [row])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.list_active_subscriptions(limit=200) == [row]
|
||||
assert service.list_active_subscriptions(limit=200) == [row]
|
||||
assert calls["count"] == 1
|
||||
|
||||
service.invalidate_subscription_cache("user-1")
|
||||
|
||||
assert service.list_active_subscriptions(limit=200) == [row]
|
||||
assert calls["count"] == 2
|
||||
|
||||
|
||||
def test_has_active_subscription_uses_lightweight_query_without_polluting_detail_cache(
|
||||
monkeypatch,
|
||||
):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(params["select"])
|
||||
if params["select"] == "expires_at":
|
||||
assert str(params["starts_at"]).startswith("lte.")
|
||||
assert params["limit"] == "1"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
)
|
||||
if params["select"] == "plan_code,source,starts_at,expires_at":
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
)
|
||||
raise AssertionError(params["select"])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
assert service.get_latest_active_subscription(
|
||||
"user-1",
|
||||
respect_requirement=False,
|
||||
)["plan_code"] == "pro_monthly"
|
||||
|
||||
assert calls == [
|
||||
"expires_at",
|
||||
"plan_code,source,starts_at,expires_at",
|
||||
]
|
||||
|
||||
|
||||
def test_has_active_subscription_lightweight_cache_invalidates(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = {"count": 0}
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls["count"] += 1
|
||||
assert params["select"] == "expires_at"
|
||||
assert str(params["starts_at"]).startswith("lte.")
|
||||
assert params["limit"] == "1"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
assert calls["count"] == 1
|
||||
|
||||
service.invalidate_subscription_cache("user-1")
|
||||
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
assert calls["count"] == 2
|
||||
|
||||
|
||||
def test_has_active_subscription_reuses_detailed_subscription_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(params["select"])
|
||||
assert params["select"] == "plan_code,source,starts_at,expires_at"
|
||||
return _Response(
|
||||
200,
|
||||
[
|
||||
{
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.get_latest_active_subscription(
|
||||
"user-1",
|
||||
respect_requirement=False,
|
||||
)["plan_code"] == "pro_monthly"
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is True
|
||||
|
||||
assert calls == ["plan_code,source,starts_at,expires_at"]
|
||||
|
||||
|
||||
def test_latest_active_subscription_reuses_negative_lightweight_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_ANON_KEY", "anon-key")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
service = SupabaseEntitlementService()
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(params["select"])
|
||||
assert params["select"] == "expires_at"
|
||||
assert str(params["starts_at"]).startswith("lte.")
|
||||
assert params["limit"] == "1"
|
||||
return _Response(200, [])
|
||||
|
||||
monkeypatch.setattr(entitlement_module.requests, "get", _fake_get)
|
||||
|
||||
assert service.has_active_subscription("user-1", respect_requirement=False) is False
|
||||
assert service.get_latest_active_subscription(
|
||||
"user-1",
|
||||
respect_requirement=False,
|
||||
) is None
|
||||
|
||||
assert calls == ["expires_at"]
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import src.database.db_manager as db_manager_module
|
||||
from src.database.db_manager import DBManager
|
||||
|
||||
|
||||
def _bound_db(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
db = DBManager(str(tmp_path / "points-sync.db"))
|
||||
db.upsert_user(1001, "eraer")
|
||||
with db._get_connection() as conn: # noqa: SLF001
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET supabase_user_id = ?, supabase_email = ?
|
||||
WHERE telegram_id = ?
|
||||
""",
|
||||
("supabase-user-1", "eraer@example.com", 1001),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO supabase_bindings (supabase_user_id, telegram_id, supabase_email)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
("supabase-user-1", 1001, "eraer@example.com"),
|
||||
)
|
||||
conn.commit()
|
||||
return db
|
||||
|
||||
|
||||
def test_message_points_sync_to_supabase_metadata_is_throttled(tmp_path, monkeypatch):
|
||||
db = _bound_db(tmp_path, monkeypatch)
|
||||
calls = []
|
||||
now = {"value": 1000.0}
|
||||
|
||||
monkeypatch.setattr(
|
||||
db_manager_module,
|
||||
"time",
|
||||
SimpleNamespace(monotonic=lambda: now["value"]),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
db_manager_module.requests,
|
||||
"patch",
|
||||
lambda *args, **kwargs: calls.append((args, kwargs))
|
||||
or SimpleNamespace(status_code=204, text="", content=b""),
|
||||
)
|
||||
|
||||
first = db.add_message_activity(
|
||||
1001,
|
||||
"第一条有效发言",
|
||||
cooldown_sec=0,
|
||||
daily_cap=1000,
|
||||
)
|
||||
now["value"] += 10.0
|
||||
second = db.add_message_activity(
|
||||
1001,
|
||||
"第二条有效发言",
|
||||
cooldown_sec=0,
|
||||
daily_cap=1000,
|
||||
)
|
||||
|
||||
assert first["awarded"] is True
|
||||
assert second["awarded"] is True
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_manual_point_grant_forces_supabase_metadata_sync(tmp_path, monkeypatch):
|
||||
db = _bound_db(tmp_path, monkeypatch)
|
||||
calls = []
|
||||
now = {"value": 1000.0}
|
||||
|
||||
monkeypatch.setattr(
|
||||
db_manager_module,
|
||||
"time",
|
||||
SimpleNamespace(monotonic=lambda: now["value"]),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
db_manager_module.requests,
|
||||
"patch",
|
||||
lambda *args, **kwargs: calls.append((args, kwargs))
|
||||
or SimpleNamespace(status_code=204, text="", content=b""),
|
||||
)
|
||||
|
||||
db.add_message_activity(1001, "第一条有效发言", cooldown_sec=0, daily_cap=1000)
|
||||
now["value"] += 10.0
|
||||
result = db.grant_points_by_supabase_email("eraer@example.com", 300)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert len(calls) == 2
|
||||
assert calls[-1][1]["json"]["user_metadata"]["points"] == result["points_after"]
|
||||
@@ -0,0 +1,83 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import src.database.db_manager as db_manager_module
|
||||
from src.database.db_manager import DBManager
|
||||
|
||||
|
||||
def _bound_db(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
monkeypatch.setenv("POLYWEATHER_SUPABASE_PROFILE_SYNC_MIN_INTERVAL_SEC", "3600")
|
||||
DBManager._profile_sync_cache.clear() # noqa: SLF001
|
||||
db = DBManager(str(tmp_path / "profile-sync.db"))
|
||||
db.upsert_user(1001, "eraer")
|
||||
with db._get_connection() as conn: # noqa: SLF001
|
||||
conn.execute(
|
||||
"""
|
||||
UPDATE users
|
||||
SET supabase_user_id = ?, supabase_email = ?
|
||||
WHERE telegram_id = ?
|
||||
""",
|
||||
("supabase-user-1", "eraer@example.com", 1001),
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO supabase_bindings (supabase_user_id, telegram_id, supabase_email)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
("supabase-user-1", 1001, "eraer@example.com"),
|
||||
)
|
||||
conn.commit()
|
||||
return db
|
||||
|
||||
|
||||
def test_repeated_user_upsert_coalesces_supabase_profile_sync(tmp_path, monkeypatch):
|
||||
db = _bound_db(tmp_path, monkeypatch)
|
||||
calls = []
|
||||
now = {"value": 1000.0}
|
||||
|
||||
monkeypatch.setattr(
|
||||
db_manager_module,
|
||||
"time",
|
||||
SimpleNamespace(monotonic=lambda: now["value"]),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
db_manager_module.requests,
|
||||
"patch",
|
||||
lambda *args, **kwargs: calls.append((args, kwargs))
|
||||
or SimpleNamespace(status_code=204, text="", content=b""),
|
||||
)
|
||||
|
||||
db.upsert_user(1001, "eraer")
|
||||
now["value"] += 10.0
|
||||
db.upsert_user(1001, "eraer")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0][1]["headers"]["Prefer"] == "return=minimal"
|
||||
|
||||
|
||||
def test_changed_username_bypasses_supabase_profile_sync_coalescing(tmp_path, monkeypatch):
|
||||
db = _bound_db(tmp_path, monkeypatch)
|
||||
calls = []
|
||||
now = {"value": 1000.0}
|
||||
|
||||
monkeypatch.setattr(
|
||||
db_manager_module,
|
||||
"time",
|
||||
SimpleNamespace(monotonic=lambda: now["value"]),
|
||||
raising=False,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
db_manager_module.requests,
|
||||
"patch",
|
||||
lambda *args, **kwargs: calls.append((args, kwargs))
|
||||
or SimpleNamespace(status_code=204, text="", content=b""),
|
||||
)
|
||||
|
||||
db.upsert_user(1001, "eraer")
|
||||
now["value"] += 10.0
|
||||
db.upsert_user(1001, "new-name")
|
||||
|
||||
assert len(calls) == 2
|
||||
assert calls[-1][1]["json"]["telegram_username"] == "new-name"
|
||||
@@ -0,0 +1,137 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _schema_sql() -> str:
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
return (root / "scripts" / "supabase" / "schema.sql").read_text(encoding="utf-8").lower()
|
||||
|
||||
|
||||
def test_supabase_schema_has_io_friendly_indexes_for_hot_ops_queries():
|
||||
schema = _schema_sql()
|
||||
|
||||
assert (
|
||||
"create index if not exists idx_profiles_email\n"
|
||||
" on public.profiles(email)\n"
|
||||
" include (id)"
|
||||
) in schema
|
||||
assert (
|
||||
"create index if not exists idx_profiles_id_lookup\n"
|
||||
" on public.profiles(id)\n"
|
||||
" include (email, created_at)"
|
||||
) in schema
|
||||
assert "idx_subscriptions_status_expiry" in schema
|
||||
assert "on public.subscriptions(expires_at asc)" in schema
|
||||
assert "idx_subscriptions_user_status_expiry" in schema
|
||||
assert "on public.subscriptions(user_id, expires_at desc)" in schema
|
||||
assert "include (id, starts_at, plan_code, source)" in schema
|
||||
assert schema.count("where status = 'active'") >= 2
|
||||
assert "idx_subscriptions_user_created" in schema
|
||||
assert "on public.subscriptions(user_id, created_at desc)" in schema
|
||||
assert "include (id, status, plan_code, source, starts_at, expires_at, updated_at)" in schema
|
||||
assert "idx_payment_intents_status_updated" in schema
|
||||
assert "on public.payment_intents(status, updated_at desc)" in schema
|
||||
assert "include (user_id)" in schema
|
||||
assert "where status in ('submitted', 'confirmed')" in schema
|
||||
assert "idx_payment_intents_user_status_updated" in schema
|
||||
assert "on public.payment_intents(user_id, status, updated_at desc)" in schema
|
||||
assert "idx_payment_intents_user_status\n" not in schema
|
||||
assert "idx_payment_intents_submitted_tx_updated" in schema
|
||||
assert "include (id, user_id, tx_hash, chain_id)" in schema
|
||||
assert "where status = 'submitted' and tx_hash is not null" in schema
|
||||
assert "idx_payment_intents_user_created" in schema
|
||||
assert "on public.payment_intents(user_id, created_at desc)" in schema
|
||||
assert "idx_payment_intents_tx_hash" in schema
|
||||
assert "on public.payment_intents(tx_hash)" in schema
|
||||
assert "include (id, user_id)" in schema
|
||||
assert "where tx_hash is not null" in schema
|
||||
assert "idx_payments_created_at" in schema
|
||||
assert "on public.payments(created_at desc)" in schema
|
||||
assert "include (id, user_id, amount, currency, chain, tx_hash, status)" in schema
|
||||
assert "idx_user_wallets_user_chain" in schema
|
||||
assert "on public.user_wallets(user_id, chain_id, is_primary desc, verified_at desc)" in schema
|
||||
assert "include (id, address)" in schema
|
||||
assert (
|
||||
"create index if not exists idx_user_wallets_chain_address_owner\n"
|
||||
" on public.user_wallets(chain_id, address)\n"
|
||||
" include (user_id, status)"
|
||||
) in schema
|
||||
assert schema.count("where status = 'active'") >= 3
|
||||
assert "idx_wallet_link_challenges_lookup" not in schema
|
||||
assert (
|
||||
"create index if not exists idx_payment_transactions_tx_hash_intent\n"
|
||||
" on public.payment_transactions(tx_hash)\n"
|
||||
" include (intent_id)"
|
||||
) in schema
|
||||
|
||||
|
||||
def test_supabase_io_budget_scripts_are_production_runnable():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
indexes = (root / "scripts" / "supabase" / "io_budget_indexes.sql").read_text(encoding="utf-8").lower()
|
||||
diagnostics = (root / "scripts" / "supabase" / "disk_io_diagnostics.sql").read_text(encoding="utf-8").lower()
|
||||
|
||||
assert "drop index if exists public.idx_profiles_email" in indexes
|
||||
assert (
|
||||
"create index if not exists idx_profiles_email\n"
|
||||
" on public.profiles(email)\n"
|
||||
" include (id)"
|
||||
) in indexes
|
||||
assert "drop index if exists public.idx_profiles_id_lookup" in indexes
|
||||
assert (
|
||||
"create index if not exists idx_profiles_id_lookup\n"
|
||||
" on public.profiles(id)\n"
|
||||
" include (email, created_at)"
|
||||
) in indexes
|
||||
assert "idx_subscriptions_user_created" in indexes
|
||||
assert "drop index if exists public.idx_subscriptions_user_status_expiry" in indexes
|
||||
assert "drop index if exists public.idx_subscriptions_status_expiry" in indexes
|
||||
assert "on public.subscriptions(user_id, expires_at desc)" in indexes
|
||||
assert "on public.subscriptions(expires_at asc)" in indexes
|
||||
assert "include (id, starts_at, plan_code, source)" in indexes
|
||||
assert "include (user_id, starts_at, plan_code)" in indexes
|
||||
assert "include (id, status, plan_code, source, starts_at, expires_at, updated_at)" in indexes
|
||||
assert "drop index if exists public.idx_payment_intents_user_status" in indexes
|
||||
assert "drop index if exists public.idx_payment_intents_status_updated" in indexes
|
||||
assert "include (user_id)" in indexes
|
||||
assert "where status in ('submitted', 'confirmed')" in indexes
|
||||
assert "drop index if exists public.idx_payment_intents_submitted_tx_updated" in indexes
|
||||
assert "include (id, user_id, tx_hash, chain_id)" in indexes
|
||||
assert "drop index if exists public.idx_payment_intents_tx_hash" in indexes
|
||||
assert "include (id, user_id)" in indexes
|
||||
assert "where tx_hash is not null" in indexes
|
||||
assert "drop index if exists public.idx_payments_created_at" in indexes
|
||||
assert "include (id, user_id, amount, currency, chain, tx_hash, status)" in indexes
|
||||
assert "drop index if exists public.idx_user_wallets_user_chain" in indexes
|
||||
assert "on public.user_wallets(user_id, chain_id, is_primary desc, verified_at desc)" in indexes
|
||||
assert "include (id, address)" in indexes
|
||||
assert "drop index if exists public.idx_user_wallets_chain_address_owner" in indexes
|
||||
assert (
|
||||
"create index if not exists idx_user_wallets_chain_address_owner\n"
|
||||
" on public.user_wallets(chain_id, address)\n"
|
||||
" include (user_id, status)"
|
||||
) in indexes
|
||||
assert "drop index if exists public.idx_wallet_link_challenges_lookup" in indexes
|
||||
assert "create index if not exists idx_wallet_link_challenges_lookup" not in indexes
|
||||
assert "drop index if exists public.idx_payment_transactions_tx_hash_intent" in indexes
|
||||
assert (
|
||||
"create index if not exists idx_payment_transactions_tx_hash_intent\n"
|
||||
" on public.payment_transactions(tx_hash)\n"
|
||||
" include (intent_id)"
|
||||
) in indexes
|
||||
assert "analyze public.payment_intents" in indexes
|
||||
assert "pg_stat_user_tables" in diagnostics
|
||||
assert "pg_stat_statements" in diagnostics
|
||||
assert "shared_blks_read" in diagnostics
|
||||
assert "pg_stat_user_indexes" in diagnostics
|
||||
assert "pg_statio_user_indexes" in diagnostics
|
||||
assert "indexrelname" in diagnostics
|
||||
assert "idx_blks_read" in diagnostics
|
||||
assert "idx_scan = 0" in diagnostics
|
||||
|
||||
|
||||
def test_supabase_setup_doc_includes_io_budget_runbook():
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
doc = (root / "docs" / "SUPABASE_SETUP_ZH.md").read_text(encoding="utf-8")
|
||||
|
||||
assert "scripts/supabase/io_budget_indexes.sql" in doc
|
||||
assert "scripts/supabase/disk_io_diagnostics.sql" in doc
|
||||
assert "Disk IO" in doc
|
||||
@@ -1,10 +1,14 @@
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.requests import Request
|
||||
|
||||
import web.core as web_core
|
||||
import web.services.auth_api as auth_api
|
||||
from web.app import app
|
||||
import web.routes as routes
|
||||
import web.services.ops_api as ops_api
|
||||
import web.scan_terminal_cache as scan_terminal_cache
|
||||
import web.scan_terminal_service as scan_terminal_service
|
||||
from web.scan_terminal_cache import scan_terminal_cache_key
|
||||
@@ -94,6 +98,138 @@ def test_payment_runtime_endpoint_returns_shape():
|
||||
assert 'recent_audit_events' in payload
|
||||
|
||||
|
||||
def test_payment_config_does_not_require_entitlement(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"_assert_entitlement",
|
||||
lambda request: (_ for _ in ()).throw(
|
||||
AssertionError("public payment config should not validate Supabase auth"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.PAYMENT_CHECKOUT,
|
||||
"get_config_payload",
|
||||
lambda: {"enabled": True, "plans": []},
|
||||
)
|
||||
|
||||
response = client.get("/api/payments/config")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["enabled"] is True
|
||||
|
||||
|
||||
def test_payment_wallets_requires_identity_without_subscription_gate(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", True)
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 0
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", lambda token: _Identity())
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"has_active_subscription",
|
||||
lambda user_id: (_ for _ in ()).throw(
|
||||
AssertionError("payment identity endpoints should not query subscription gate"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "list_wallets", lambda user_id: [])
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "chain_id", 137)
|
||||
|
||||
response = client.get(
|
||||
"/api/payments/wallets",
|
||||
headers={"Authorization": "Bearer access-token"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"wallets": [], "chain_id": 137}
|
||||
|
||||
|
||||
def test_telegram_identity_endpoints_skip_subscription_gate(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", True)
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 0
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
class _FakePricing:
|
||||
configured = True
|
||||
|
||||
@staticmethod
|
||||
def verify_login_payload(payload):
|
||||
return {"telegram_id": int(payload["id"]), "username": "tester"}
|
||||
|
||||
@staticmethod
|
||||
def get_member_status(telegram_id):
|
||||
return "member"
|
||||
|
||||
@staticmethod
|
||||
def resolve_price_for_telegram_id(telegram_id):
|
||||
return {"telegram_id": telegram_id, "pricing_source": "telegram_group_member"}
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def upsert_user(telegram_id, username):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def bind_supabase_identity(*, telegram_id, supabase_user_id, supabase_email):
|
||||
return {"ok": True}
|
||||
|
||||
@staticmethod
|
||||
def consume_bind_token(token):
|
||||
return 12345
|
||||
|
||||
@staticmethod
|
||||
def create_web_bind_token(*, supabase_user_id, supabase_email, ttl_minutes):
|
||||
return "bind-token"
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", lambda token: _Identity())
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"has_active_subscription",
|
||||
lambda user_id: (_ for _ in ()).throw(
|
||||
AssertionError("telegram identity endpoints should not query subscription gate"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(auth_api, "TelegramGroupPricing", lambda: _FakePricing())
|
||||
monkeypatch.setattr(auth_api, "DBManager", lambda: _FakeDB())
|
||||
|
||||
auth_headers = {"Authorization": "Bearer access-token"}
|
||||
|
||||
login_response = client.post(
|
||||
"/api/auth/telegram/login",
|
||||
headers=auth_headers,
|
||||
json={"id": 12345, "username": "tester", "auth_date": 1770000000, "hash": "x" * 64},
|
||||
)
|
||||
assert login_response.status_code == 200
|
||||
|
||||
token_response = client.post(
|
||||
"/api/auth/telegram/bind-by-token",
|
||||
headers=auth_headers,
|
||||
json={"token": "token-12345"},
|
||||
)
|
||||
assert token_response.status_code == 200
|
||||
|
||||
link_response = client.post(
|
||||
"/api/auth/telegram/bot-bind-link",
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert link_response.status_code == 200
|
||||
|
||||
|
||||
def test_auth_me_does_not_reconcile_on_status_probe(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
|
||||
@@ -106,21 +242,21 @@ def test_auth_me_does_not_reconcile_on_status_probe(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
|
||||
calls = {"count": 0}
|
||||
reconcile_calls = {"count": 0}
|
||||
|
||||
def _latest_subscription(user_id, respect_requirement=False):
|
||||
calls["count"] += 1
|
||||
def _subscription_window(user_id, respect_requirement=False):
|
||||
return {
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-22T00:00:00+00:00",
|
||||
"expires_at": "2026-04-21T00:00:00+00:00",
|
||||
"current": {
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-22T00:00:00+00:00",
|
||||
"expires_at": "2026-04-21T00:00:00+00:00",
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_active_subscription",
|
||||
_latest_subscription,
|
||||
"get_subscription_window",
|
||||
_subscription_window,
|
||||
)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", True)
|
||||
|
||||
@@ -143,6 +279,265 @@ def test_auth_me_does_not_reconcile_on_status_probe(monkeypatch):
|
||||
assert reconcile_calls["count"] == 0
|
||||
|
||||
|
||||
def test_auth_me_reuses_identity_bound_by_entitlement(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", True)
|
||||
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 7
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
calls = {"identity": 0}
|
||||
|
||||
def _get_identity(token):
|
||||
calls["identity"] += 1
|
||||
assert token == "access-token"
|
||||
return _Identity()
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", _get_identity)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "has_active_subscription", lambda user_id: True)
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
lambda user_id, respect_requirement=False: {
|
||||
"current": {
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-22T00:00:00+00:00",
|
||||
"expires_at": "2026-04-21T00:00:00+00:00",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/auth/me",
|
||||
headers={"Authorization": "Bearer access-token"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["authenticated"] is True
|
||||
assert payload["points"] == 7
|
||||
assert calls["identity"] == 1
|
||||
|
||||
|
||||
def test_auth_me_uses_subscription_window_as_required_subscription_gate(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", True)
|
||||
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 7
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", lambda token: _Identity())
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"has_active_subscription",
|
||||
lambda user_id: (_ for _ in ()).throw(
|
||||
AssertionError("auth/me should not run a separate lightweight subscription gate"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_active_subscription",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("auth/me should derive current subscription from the window query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
lambda user_id, respect_requirement=False: {
|
||||
"current": {
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-22T00:00:00+00:00",
|
||||
"expires_at": "2026-04-21T00:00:00+00:00",
|
||||
},
|
||||
"total_expires_at": "2026-05-21T00:00:00+00:00",
|
||||
"queued_days": 30,
|
||||
"queued_count": 1,
|
||||
},
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/auth/me",
|
||||
headers={"Authorization": "Bearer access-token"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["subscription_active"] is True
|
||||
assert payload["subscription_plan_code"] == "pro_monthly"
|
||||
assert payload["subscription_queued_days"] == 30
|
||||
|
||||
|
||||
def test_auth_me_uses_window_rows_for_non_required_latest_known_subscription(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", False)
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", False)
|
||||
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||
monkeypatch.setattr(routes, "_resolve_auth_points", lambda request: 0)
|
||||
|
||||
def _bind_identity(request):
|
||||
request.state.auth_user_id = "user-1"
|
||||
request.state.auth_email = "user@example.com"
|
||||
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_bind_optional_supabase_identity", _bind_identity)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
lambda user_id, respect_requirement=False: {
|
||||
"current": None,
|
||||
"rows": [
|
||||
{
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-06-01T00:00:00+00:00",
|
||||
"expires_at": "2026-07-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
"total_expires_at": "2026-07-01T00:00:00+00:00",
|
||||
"queued_days": 0,
|
||||
"queued_count": 0,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_active_subscription",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("auth/me should reuse window rows before latest active fallback"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_subscription_any_status",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("auth/me should reuse window rows before historical fallback"),
|
||||
),
|
||||
)
|
||||
|
||||
response = client.get("/api/auth/me")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["subscription_active"] is False
|
||||
assert payload["subscription_plan_code"] == "pro_monthly"
|
||||
assert payload["subscription_expires_at"] == "2026-07-01T00:00:00+00:00"
|
||||
|
||||
|
||||
def test_auth_me_skips_latest_active_after_empty_non_required_window(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", False)
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", False)
|
||||
monkeypatch.setattr(routes, "_resolve_weekly_profile", lambda request: {"weekly_points": 0, "weekly_rank": None})
|
||||
monkeypatch.setattr(routes, "_resolve_auth_points", lambda request: 0)
|
||||
|
||||
def _bind_identity(request):
|
||||
request.state.auth_user_id = "user-1"
|
||||
request.state.auth_email = "user@example.com"
|
||||
|
||||
latest_any_calls = {"count": 0}
|
||||
|
||||
def _latest_any_status(user_id):
|
||||
latest_any_calls["count"] += 1
|
||||
return {
|
||||
"plan_code": "expired_pro",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_bind_optional_supabase_identity", _bind_identity)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
lambda user_id, respect_requirement=False: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_active_subscription",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("empty subscription window should skip latest active fallback"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_subscription_any_status",
|
||||
_latest_any_status,
|
||||
)
|
||||
|
||||
response = client.get("/api/auth/me")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["subscription_active"] is False
|
||||
assert payload["subscription_plan_code"] == "expired_pro"
|
||||
assert latest_any_calls["count"] == 1
|
||||
|
||||
|
||||
def test_auth_me_preserves_required_subscription_403_from_window(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "require_subscription", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", True)
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 0
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", lambda token: _Identity())
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"has_active_subscription",
|
||||
lambda user_id: (_ for _ in ()).throw(
|
||||
AssertionError("auth/me should not run a separate lightweight subscription gate"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
lambda user_id, respect_requirement=False: {},
|
||||
)
|
||||
latest_any_calls = {"count": 0}
|
||||
|
||||
def _latest_any_status(user_id):
|
||||
latest_any_calls["count"] += 1
|
||||
return {
|
||||
"plan_code": "expired_pro",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
web_core.SUPABASE_ENTITLEMENT,
|
||||
"get_latest_subscription_any_status",
|
||||
_latest_any_status,
|
||||
)
|
||||
|
||||
response = client.get(
|
||||
"/api/auth/me",
|
||||
headers={"Authorization": "Bearer access-token"},
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.json()["detail"] == "Subscription required"
|
||||
assert latest_any_calls["count"] == 0
|
||||
|
||||
|
||||
def test_backend_entitlement_token_binds_forwarded_supabase_identity(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
@@ -167,6 +562,45 @@ def test_backend_entitlement_token_binds_forwarded_supabase_identity(monkeypatch
|
||||
assert request.state.auth_email == "user@example.com"
|
||||
|
||||
|
||||
def test_backend_entitlement_token_without_forwarded_identity_validates_bearer(monkeypatch):
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "enabled", True)
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "supabase_url", "https://example.supabase.co")
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "anon_key", "anon-key")
|
||||
monkeypatch.setattr(web_core, "_SUPABASE_AUTH_REQUIRED", False)
|
||||
monkeypatch.setattr(web_core, "_ENTITLEMENT_TOKEN", "backend-token")
|
||||
|
||||
class _Identity:
|
||||
user_id = "user-1"
|
||||
email = "user@example.com"
|
||||
points = 7
|
||||
created_at = "2026-05-01T00:00:00+00:00"
|
||||
|
||||
calls = {"count": 0}
|
||||
|
||||
def _get_identity(token):
|
||||
calls["count"] += 1
|
||||
assert token == "access-token"
|
||||
return _Identity()
|
||||
|
||||
monkeypatch.setattr(web_core.SUPABASE_ENTITLEMENT, "get_identity", _get_identity)
|
||||
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"headers": [
|
||||
(b"x-polyweather-entitlement", b"backend-token"),
|
||||
(b"authorization", b"Bearer access-token"),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
web_core._assert_entitlement(request)
|
||||
|
||||
assert calls["count"] == 1
|
||||
assert request.state.auth_user_id == "user-1"
|
||||
assert request.state.auth_email == "user@example.com"
|
||||
|
||||
|
||||
def test_ops_memberships_prefers_supabase_auth_email(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
@@ -216,6 +650,504 @@ def test_ops_memberships_prefers_supabase_auth_email(monkeypatch):
|
||||
assert payload["memberships"][0]["email"] == "fresh@example.com"
|
||||
|
||||
|
||||
def test_ops_memberships_uses_batched_subscription_windows(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", False)
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def get_users_by_supabase_user_ids(user_ids):
|
||||
return {
|
||||
"user-1": {"supabase_email": "one@example.com"},
|
||||
"user-2": {"supabase_email": "two@example.com"},
|
||||
}
|
||||
|
||||
import src.database.db_manager as db_module
|
||||
|
||||
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscriptions",
|
||||
lambda limit=200: [
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"user_id": "user-2",
|
||||
"plan_code": "pro_monthly",
|
||||
"starts_at": "2026-03-02T00:00:00+00:00",
|
||||
"expires_at": "2026-04-02T00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
)
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "get_auth_users", lambda user_ids: {})
|
||||
|
||||
def _fail_per_user_window(*args, **kwargs):
|
||||
raise AssertionError("per-user subscription window query should not run")
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"get_subscription_window",
|
||||
_fail_per_user_window,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_subscription_windows",
|
||||
lambda user_ids, bypass_cache=True: {
|
||||
"user-1": {"total_expires_at": "2026-04-15T00:00:00+00:00", "queued_days": 14, "queued_count": 1},
|
||||
"user-2": {"total_expires_at": "2026-04-02T00:00:00+00:00", "queued_days": 0, "queued_count": 0},
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
response = client.get("/api/ops/memberships")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
rows = {row["user_id"]: row for row in payload["memberships"]}
|
||||
assert rows["user-1"]["queued_days"] == 14
|
||||
assert rows["user-2"]["queued_days"] == 0
|
||||
|
||||
|
||||
def test_ops_memberships_prefers_single_active_subscription_window_query(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", False)
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def get_users_by_supabase_user_ids(user_ids):
|
||||
return {"user-1": {"supabase_email": "one@example.com"}}
|
||||
|
||||
import src.database.db_manager as db_module
|
||||
|
||||
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscriptions",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("memberships should not run a separate active subscription query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_subscription_windows",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("memberships should not run a second window query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "get_auth_users", lambda user_ids: {})
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscription_windows",
|
||||
lambda limit=200: {
|
||||
"subscriptions": [
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
"windows": {
|
||||
"user-1": {
|
||||
"total_expires_at": "2026-05-01T00:00:00+00:00",
|
||||
"queued_days": 30,
|
||||
"queued_count": 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
response = client.get("/api/ops/memberships")
|
||||
|
||||
assert response.status_code == 200
|
||||
row = response.json()["memberships"][0]
|
||||
assert row["queued_days"] == 30
|
||||
assert row["expires_at"] == "2026-05-01T00:00:00+00:00"
|
||||
|
||||
|
||||
def test_ops_memberships_growth_reuses_active_subscription_window_query(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
|
||||
starts_at = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscriptions",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("growth should not run a separate active subscription query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscription_windows",
|
||||
lambda limit=5000: {
|
||||
"subscriptions": [
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": starts_at,
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
"windows": {},
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
response = client.get("/api/ops/memberships/growth?days=7")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert any(day["paid"] == 1 for day in response.json()["daily"])
|
||||
|
||||
|
||||
def test_ops_telegram_audit_reuses_active_subscription_window_query(monkeypatch):
|
||||
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "bot-token")
|
||||
monkeypatch.setenv("TELEGRAM_CHAT_IDS", "chat-1")
|
||||
monkeypatch.delenv("TELEGRAM_CHAT_ID", raising=False)
|
||||
monkeypatch.delenv("POLYWEATHER_TELEGRAM_GROUP_ID", raising=False)
|
||||
monkeypatch.delenv("POLYWEATHER_TELEGRAM_TOPICS_GROUP_ID", raising=False)
|
||||
monkeypatch.setattr(ops_api, "_require_ops", lambda request: None)
|
||||
|
||||
class _FakeRows(list):
|
||||
def fetchall(self):
|
||||
return self
|
||||
|
||||
class _FakeConnection:
|
||||
row_factory = None
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
return False
|
||||
|
||||
def execute(self, sql):
|
||||
if "FROM users" in sql:
|
||||
return _FakeRows([{"telegram_id": 1, "username": "tester"}])
|
||||
if "FROM supabase_bindings" in sql:
|
||||
return _FakeRows(
|
||||
[
|
||||
{
|
||||
"telegram_id": 1,
|
||||
"supabase_user_id": "user-1",
|
||||
"supabase_email": "one@example.com",
|
||||
}
|
||||
]
|
||||
)
|
||||
raise AssertionError(sql)
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def _get_connection():
|
||||
return _FakeConnection()
|
||||
|
||||
class _Response:
|
||||
status_code = 200
|
||||
|
||||
@staticmethod
|
||||
def json():
|
||||
return {"ok": True, "result": {"status": "member"}}
|
||||
|
||||
import requests as requests_module
|
||||
import src.database.db_manager as db_module
|
||||
|
||||
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
|
||||
monkeypatch.setattr(requests_module, "get", lambda *args, **kwargs: _Response())
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscriptions",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("telegram audit should not run a separate active subscription query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscription_windows",
|
||||
lambda limit=5000: {
|
||||
"subscriptions": [
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": "2026-03-01T00:00:00+00:00",
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
}
|
||||
],
|
||||
"windows": {},
|
||||
},
|
||||
raising=False,
|
||||
)
|
||||
|
||||
result = ops_api.get_ops_telegram_audit(object())
|
||||
|
||||
assert result["valid_count"] == 1
|
||||
assert result["anomaly_count"] == 0
|
||||
|
||||
|
||||
def test_ops_memberships_overview_combines_memberships_and_growth_in_one_subscription_query(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", False)
|
||||
|
||||
starts_at = datetime.utcnow().replace(microsecond=0).isoformat()
|
||||
calls = {"active_windows": 0}
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def get_users_by_supabase_user_ids(user_ids):
|
||||
return {"user-1": {"supabase_email": "one@example.com"}}
|
||||
|
||||
import src.database.db_manager as db_module
|
||||
|
||||
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscriptions",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("overview should not run a separate active subscription query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_subscription_windows",
|
||||
lambda *args, **kwargs: (_ for _ in ()).throw(
|
||||
AssertionError("overview should not run a second window query"),
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "get_auth_users", lambda user_ids: {})
|
||||
|
||||
def _active_windows(limit=5000):
|
||||
calls["active_windows"] += 1
|
||||
assert limit == 5000
|
||||
return {
|
||||
"subscriptions": [
|
||||
{
|
||||
"user_id": "user-1",
|
||||
"plan_code": "pro_monthly",
|
||||
"source": "payment_contract",
|
||||
"starts_at": starts_at,
|
||||
"expires_at": "2099-01-01T00:00:00+00:00",
|
||||
},
|
||||
{
|
||||
"user_id": "user-2",
|
||||
"plan_code": "signup_trial_3d",
|
||||
"source": "signup_trial",
|
||||
"starts_at": starts_at,
|
||||
"expires_at": "2099-01-02T00:00:00+00:00",
|
||||
},
|
||||
],
|
||||
"windows": {
|
||||
"user-1": {
|
||||
"total_expires_at": "2099-01-01T00:00:00+00:00",
|
||||
"queued_days": 0,
|
||||
"queued_count": 0,
|
||||
},
|
||||
"user-2": {
|
||||
"total_expires_at": "2099-01-02T00:00:00+00:00",
|
||||
"queued_days": 0,
|
||||
"queued_count": 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes.SUPABASE_ENTITLEMENT,
|
||||
"list_active_subscription_windows",
|
||||
_active_windows,
|
||||
raising=False,
|
||||
)
|
||||
|
||||
response = client.get("/api/ops/memberships/overview?limit=1&days=7")
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert len(payload["memberships"]) == 1
|
||||
assert payload["memberships"][0]["user_id"] == "user-1"
|
||||
assert any(day["paid"] == 1 and day["trial"] == 1 for day in payload["daily"])
|
||||
assert calls["active_windows"] == 1
|
||||
|
||||
|
||||
def test_ops_memberships_does_not_reconcile_payments_by_default(monkeypatch):
|
||||
monkeypatch.delenv("POLYWEATHER_OPS_MEMBERSHIPS_RECONCILE_ENABLED", raising=False)
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "enabled", True)
|
||||
|
||||
calls = {"count": 0}
|
||||
|
||||
def _count_reconcile(*args, **kwargs):
|
||||
calls["count"] += 1
|
||||
return {"ok": True}
|
||||
|
||||
monkeypatch.setattr(routes.PAYMENT_CHECKOUT, "reconcile_recent_intents", _count_reconcile)
|
||||
|
||||
class _FakeDB:
|
||||
@staticmethod
|
||||
def get_users_by_supabase_user_ids(user_ids):
|
||||
return {}
|
||||
|
||||
import src.database.db_manager as db_module
|
||||
|
||||
monkeypatch.setattr(db_module, "DBManager", lambda: _FakeDB())
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "list_active_subscriptions", lambda limit=200: [])
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "get_auth_users", lambda user_ids: {})
|
||||
monkeypatch.setattr(routes.SUPABASE_ENTITLEMENT, "list_subscription_windows", lambda user_ids, bypass_cache=True: {})
|
||||
|
||||
response = client.get("/api/ops/memberships")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["memberships"] == []
|
||||
assert calls["count"] == 0
|
||||
|
||||
|
||||
def test_ops_email_lookup_prefers_profiles_over_auth_admin(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
|
||||
calls = []
|
||||
|
||||
class _Response:
|
||||
ok = True
|
||||
status_code = 200
|
||||
content = b"1"
|
||||
text = ""
|
||||
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append((url, params))
|
||||
if url.endswith("/rest/v1/profiles"):
|
||||
assert headers is not None
|
||||
assert headers.get("Prefer") != "return=representation"
|
||||
assert params["select"] == "id"
|
||||
assert params["email"] == "eq.user@example.com"
|
||||
return _Response([{"id": "user-1"}])
|
||||
raise AssertionError(f"unexpected auth admin lookup: {url}")
|
||||
|
||||
monkeypatch.setattr(ops_api._requests, "get", _fake_get)
|
||||
|
||||
assert (
|
||||
ops_api._lookup_supabase_user_id_by_email(
|
||||
"https://example.supabase.co",
|
||||
"service-role",
|
||||
"user@example.com",
|
||||
)
|
||||
== "user-1"
|
||||
)
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_ops_subscription_grant_invalidates_subscription_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
monkeypatch.setattr(ops_api, "_require_ops", lambda request: {"email": "admin@example.com"})
|
||||
|
||||
class _Response:
|
||||
ok = True
|
||||
status_code = 200
|
||||
content = b"1"
|
||||
text = ""
|
||||
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
assert url.endswith("/rest/v1/profiles")
|
||||
assert params["select"] == "id"
|
||||
return _Response([{"id": "user-1"}])
|
||||
|
||||
def _fake_post(url, headers=None, json=None, timeout=None):
|
||||
assert url.endswith("/rest/v1/subscriptions")
|
||||
assert headers["Prefer"] == "return=minimal"
|
||||
return _Response([{"id": 1, "user_id": "user-1"}])
|
||||
|
||||
invalidated = []
|
||||
monkeypatch.setattr(ops_api._requests, "get", _fake_get)
|
||||
monkeypatch.setattr(ops_api._requests, "post", _fake_post)
|
||||
monkeypatch.setattr(
|
||||
ops_api.legacy_routes.SUPABASE_ENTITLEMENT,
|
||||
"invalidate_subscription_cache",
|
||||
lambda user_id: invalidated.append(user_id),
|
||||
)
|
||||
|
||||
result = ops_api.grant_ops_subscription(object(), "user@example.com")
|
||||
|
||||
assert result["ok"] is True
|
||||
assert invalidated == ["user-1"]
|
||||
|
||||
|
||||
def test_ops_subscription_extend_uses_minimal_return_and_invalidates_cache(monkeypatch):
|
||||
monkeypatch.setenv("SUPABASE_URL", "https://example.supabase.co")
|
||||
monkeypatch.setenv("SUPABASE_SERVICE_ROLE_KEY", "service-role")
|
||||
monkeypatch.setattr(ops_api, "_require_ops", lambda request: {"email": "admin@example.com"})
|
||||
|
||||
class _Response:
|
||||
ok = True
|
||||
status_code = 200
|
||||
content = b"1"
|
||||
text = ""
|
||||
|
||||
def __init__(self, payload):
|
||||
self._payload = payload
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
if url.endswith("/rest/v1/profiles"):
|
||||
assert params["select"] == "id"
|
||||
return _Response([{"id": "user-1"}])
|
||||
if url.endswith("/rest/v1/subscriptions"):
|
||||
assert params["select"] == "id,expires_at"
|
||||
return _Response(
|
||||
[
|
||||
{
|
||||
"id": 7,
|
||||
"expires_at": "2026-04-01T00:00:00+00:00",
|
||||
}
|
||||
]
|
||||
)
|
||||
raise AssertionError(url)
|
||||
|
||||
def _fake_patch(url, headers=None, json=None, timeout=None):
|
||||
assert url.endswith("/rest/v1/subscriptions?id=eq.7")
|
||||
assert headers["Prefer"] == "return=minimal"
|
||||
assert "expires_at" in json
|
||||
return _Response([])
|
||||
|
||||
invalidated = []
|
||||
monkeypatch.setattr(ops_api._requests, "get", _fake_get)
|
||||
monkeypatch.setattr(ops_api._requests, "patch", _fake_patch)
|
||||
monkeypatch.setattr(
|
||||
ops_api.legacy_routes.SUPABASE_ENTITLEMENT,
|
||||
"invalidate_subscription_cache",
|
||||
lambda user_id: invalidated.append(user_id),
|
||||
)
|
||||
|
||||
result = ops_api.extend_ops_subscription(object(), "user@example.com", additional_days=7)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["new_expires_at"].startswith("2026-04-08")
|
||||
assert invalidated == ["user-1"]
|
||||
|
||||
|
||||
def test_ops_truth_history_returns_filtered_rows(monkeypatch):
|
||||
monkeypatch.setattr(routes, "_assert_entitlement", lambda request: None)
|
||||
monkeypatch.setattr(routes, "_require_ops_admin", lambda request: None)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from datetime import datetime
|
||||
|
||||
import src.bot.weekly_reward_loop as weekly_reward_loop
|
||||
|
||||
|
||||
class _Response:
|
||||
def __init__(self, status_code=200, payload=None):
|
||||
self.status_code = status_code
|
||||
self._payload = payload
|
||||
self.content = b"1"
|
||||
|
||||
def json(self):
|
||||
return self._payload
|
||||
|
||||
|
||||
def test_weekly_reward_bonus_subscription_insert_uses_minimal_return(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def _fake_get(url, headers=None, params=None, timeout=None):
|
||||
calls.append(("GET", url, headers, params))
|
||||
return _Response(200, [])
|
||||
|
||||
def _fake_post(url, headers=None, json=None, timeout=None):
|
||||
calls.append(("POST", url, headers, json))
|
||||
assert headers["Prefer"] == "return=minimal"
|
||||
return _Response(201, [])
|
||||
|
||||
monkeypatch.setattr(weekly_reward_loop.requests, "get", _fake_get)
|
||||
monkeypatch.setattr(weekly_reward_loop.requests, "post", _fake_post)
|
||||
|
||||
ok, reason, expires_at = weekly_reward_loop._grant_bonus_subscription_days(
|
||||
supabase_url="https://example.supabase.co",
|
||||
service_role_key="service-role",
|
||||
user_id="user-1",
|
||||
days=7,
|
||||
timeout_sec=5,
|
||||
)
|
||||
|
||||
assert ok is True
|
||||
assert reason == ""
|
||||
assert datetime.fromisoformat(str(expires_at)).tzinfo is not None
|
||||
assert [call[0] for call in calls] == ["GET", "POST"]
|
||||
Reference in New Issue
Block a user