Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lnurl/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,10 @@ def is_valid_amount(self, amount: int) -> bool:


def is_pay_action_response(data: dict) -> bool:
return "pr" in data and "routes" in data
# LUD-06: LnurlPayActionResponse is identified by just the presence of "pr".
# "routes" is specified as required in the spec as empty array;
# some services falsely omit it, so we are only checking for "pr".
return "pr" in data


class LnurlResponse:
Expand Down
78 changes: 78 additions & 0 deletions tests/test_pay_action_response_lud06.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from lnurl import LnurlErrorResponse
from lnurl.models import LnurlPayActionResponse, LnurlResponse


def test_from_dict_pay_action_response_without_routes():
"""
LUD-06 success form: callback response that only contains `pr`
(and optional fields like `successAction`, `disposable`) MUST
be parsed as LnurlPayActionResponse even when `routes` is omitted.
"""
data = {
"pr": (
"lnbc1pnsu5z3pp57getmdaxhg5kc9yh2a2qsh7cjf4gnccgkw0qenm8vsqv50w7s"
"ygqdqj0fjhymeqv9kk7atwwscqzzsxqyz5vqsp5e2yyqcp0a3ujeesp24ya0glej"
"srh703md8mrx0g2lyvjxy5w27ss9qxpqysgqyjreasng8a086kpkczv48er5c6l5"
"73aym6ynrdl9nkzqnag49vt3sjjn8qdfq5cr6ha0vrdz5c5r3v4aghndly0hplmv"
"6hjxepwp93cq398l3s"
),
"successAction": {
"tag": "message",
"message": "LNURL pay to user@example.com",
},
"disposable": False,
# NOTE: `routes` is intentionally omitted here.
}

res = LnurlResponse.from_dict(data)

assert isinstance(res, LnurlPayActionResponse)
assert res.pr == data["pr"]
assert res.successAction is not None
assert res.disposable is False


def test_from_dict_pay_action_response_pr_only_no_status():
"""
Regression test for the original reported error:
LnurlResponseException: Expected Success or Error response. But no 'status' given.

A callback response body that contains only `pr` (no `routes`, no `status`) must
not raise that exception — it must be parsed as LnurlPayActionResponse.
Even though LUD06 specified an required empty `routes` array some services omit it.
"""
data = {
"pr": (
"lnbc1pnsu5z3pp57getmdaxhg5kc9yh2a2qsh7cjf4gnccgkw0qenm8vsqv50w7s"
"ygqdqj0fjhymeqv9kk7atwwscqzzsxqyz5vqsp5e2yyqcp0a3ujeesp24ya0glej"
"srh703md8mrx0g2lyvjxy5w27ss9qxpqysgqyjreasng8a086kpkczv48er5c6l5"
"73aym6ynrdl9nkzqnag49vt3sjjn8qdfq5cr6ha0vrdz5c5r3v4aghndly0hplmv"
"6hjxepwp93cq398l3s"
),
# NOTE: no `routes`, no `status` — the minimal real-world payload that
# triggered "Expected Success or Error response. But no 'status' given."
}

res = LnurlResponse.from_dict(data)

assert isinstance(res, LnurlPayActionResponse)
assert res.pr == data["pr"]
assert res.routes == []


def test_from_dict_error_response_with_status_error():
"""
LUD-06 error form: any response shaped as
{"status": "ERROR", "reason": "error details..."}
must be parsed as LnurlErrorResponse with the correct reason.
"""
data = {
"status": "ERROR",
"reason": "error details...",
}

res = LnurlResponse.from_dict(data)

assert isinstance(res, LnurlErrorResponse)
assert res.reason == "error details..."
assert res.ok is False
Loading