From e98ee941c3ce0fdb9fda48e667852e9caa78a96a Mon Sep 17 00:00:00 2001 From: Wired4ncer <102553581+Wired4ncer@users.noreply.github.com> Date: Wed, 22 Jul 2026 13:30:22 -0600 Subject: [PATCH] Guard verify_order_and_connection against unknown/inactive ad id An order can reference an ad id we don't have (typo/unknown), or one that was just inactivated; before any ad is published active_ads is None. The bare `self.ad_handler.active_ads.ads[order.d]` raised KeyError/AttributeError, and because orders run as fire-and-forget asyncio tasks the exception was swallowed so the client got no response at all. Return an OrderErrorResponse instead. Adds tests/test_order_handler_unknown_ad.py (no node/relay required). Co-Authored-By: Claude Opus 4.8 --- publsp/marketplace/lsp.py | 13 +++++++- tests/test_order_handler_unknown_ad.py | 44 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tests/test_order_handler_unknown_ad.py diff --git a/publsp/marketplace/lsp.py b/publsp/marketplace/lsp.py index e3d6309..e878d5f 100644 --- a/publsp/marketplace/lsp.py +++ b/publsp/marketplace/lsp.py @@ -358,7 +358,18 @@ def __init__( async def verify_order_and_connection( self, order: Order) -> Union[OrderResponse, None]: - ad = self.ad_handler.active_ads.ads[order.d] + # the order may reference an ad id we don't have (unknown/typo), or one + # that was just inactivated; guard the lookup so a stray request can't + # crash the fire-and-forget order task with a KeyError/AttributeError + active_ads = getattr(self.ad_handler, 'active_ads', None) + ad = active_ads.ads.get(order.d) if active_ads else None + if ad is None: + logger.error( + f"order references unknown or inactive ad id '{order.d}', cancelling") + return OrderErrorResponse( + code=OrderErrorCode.invalid_params, + error_message="unknown or no longer active offer id", + ) # validate the order request first checked_order = order.validate_order(ad=ad) if not checked_order.is_valid: diff --git a/tests/test_order_handler_unknown_ad.py b/tests/test_order_handler_unknown_ad.py new file mode 100644 index 0000000..d8f7762 --- /dev/null +++ b/tests/test_order_handler_unknown_ad.py @@ -0,0 +1,44 @@ +""" +Regression tests: an order referencing an unknown or no-longer-active ad id must +be answered with an OrderErrorResponse rather than crashing the order task. + +verify_order_and_connection previously did a bare +`self.ad_handler.active_ads.ads[order.d]`, which raised KeyError (unknown id) or +AttributeError (no ad published yet, active_ads is None). Because orders are +handled as fire-and-forget asyncio tasks, that exception was swallowed and the +requesting client got no response at all. + +These construct an OrderHandler with stub dependencies; the guarded path returns +before any Lightning-node call, so no node/relay is required. +""" +from types import SimpleNamespace + +import pytest + +from publsp.blip51.order import Order, OrderErrorResponse +from publsp.marketplace.lsp import OrderHandler + + +def _order_handler(active_ads) -> OrderHandler: + return OrderHandler( + ln_backend=None, + ad_handler=SimpleNamespace(active_ads=active_ads), + rumor_handler=None, + nostr_client=None, + ) + + +@pytest.mark.asyncio +async def test_no_ad_published_returns_error(): + # active_ads is None before any ad has been published + handler = _order_handler(active_ads=None) + result = await handler.verify_order_and_connection(Order(d="any-id")) + assert isinstance(result, OrderErrorResponse) + + +@pytest.mark.asyncio +async def test_unknown_ad_id_returns_error(): + # an ad set exists but does not contain the requested id + handler = _order_handler(active_ads=SimpleNamespace(ads={})) + result = await handler.verify_order_and_connection(Order(d="does-not-exist")) + assert isinstance(result, OrderErrorResponse)