nwcprovider/tests/unit/test_nwcp.py
2026-08-26 13:06:39 +02:00

326 lines
9.9 KiB
Python

import asyncio
import json
import random
import string
import time
import pytest
from loguru import logger
from ...nwcp import NWCServiceProvider
@pytest.fixture
def nwc_service_provider():
return NWCServiceProvider(
"d7b5232fba0e02e32cfe26f20cdf2c803b27ecd81052c2dd5d17e5e1a333fe58", ""
)
@pytest.fixture
def nwc_service_provider2():
return NWCServiceProvider(
"ce40821040275f72f3074a89770db3e2744b189f204807c867840eb58565de51", ""
)
def test_supported_methods(nwc_service_provider):
def make_invoice(provider, pubkey, content):
return "invoice"
nwc_service_provider.add_request_listener("make_invoice", make_invoice)
s = nwc_service_provider.get_supported_methods()
assert s == ["make_invoice"]
def test_encrytdecrypt(nwc_service_provider, nwc_service_provider2):
content = "Hello World"
enc_a = nwc_service_provider.private_key.encrypt_message(
content, nwc_service_provider2.public_key_hex
)
enc_b = nwc_service_provider2.private_key.encrypt_message(
content, nwc_service_provider.public_key_hex
)
dec_a = nwc_service_provider2.private_key.decrypt_message(
enc_a, nwc_service_provider.public_key_hex
)
dec_b = nwc_service_provider.private_key.decrypt_message(
enc_b, nwc_service_provider2.public_key_hex
)
assert dec_a == content
assert dec_b == content
def test_signverify(nwc_service_provider, nwc_service_provider2):
# Random content
content = ""
for _ in range(100):
content += random.choice(string.ascii_letters)
tags = []
for _ in range(random.choice([1, 2, 3, 4])):
tags.append(
[
random.choice(string.ascii_letters)
+ "_"
+ random.choice(string.ascii_letters),
random.choice(string.ascii_letters),
]
)
event = {"kind": 1, "content": content, "tags": tags, "created_at": 1234567890}
signed = nwc_service_provider._sign_event(event)
assert nwc_service_provider2._verify_event(signed)
def test_default_event_max_age(nwc_service_provider):
assert nwc_service_provider.event_max_age == 5 * 60
assert (
NWCServiceProvider(
"d7b5232fba0e02e32cfe26f20cdf2c803b27ecd81052c2dd5d17e5e1a333fe58",
"",
handle_missed_events=123,
).event_max_age
== 123
)
@pytest.mark.asyncio
async def test_handle(nwc_service_provider, nwc_service_provider2):
content = nwc_service_provider._json_dumps(
{"method": "pay_invoice", "params": {"invoice": "abc"}}
)
content = nwc_service_provider.private_key.encrypt_message(
content, nwc_service_provider2.public_key_hex
)
event = {
"kind": 23194,
"content": content,
"tags": [["p", nwc_service_provider2.public_key_hex]],
"created_at": int(time.time()),
}
signed = nwc_service_provider._sign_event(event)
async def _handle_pay_invoice(provider, pubkey, content):
assert pubkey == nwc_service_provider.public_key_hex
assert content["method"] == "pay_invoice"
assert content["params"]["invoice"] == "abc"
return [({"preimage": "00000"}, None, [["r1", "v1"]])]
async def _send_pass(obj):
pass
nwc_service_provider2._send = _send_pass
nwc_service_provider2._create_subscription()
nwc_service_provider2.add_request_listener("pay_invoice", _handle_pay_invoice)
sent_events = await nwc_service_provider2._handle_request(signed)
assert len(sent_events) == 1
for revent in sent_events:
assert nwc_service_provider2._verify_event(revent)
content = nwc_service_provider2.private_key.decrypt_message(
revent["content"], nwc_service_provider.public_key_hex
)
logger.debug(event)
logger.debug(revent)
content = json.loads(content)
assert content["result_type"] == "pay_invoice"
assert content["result"]["preimage"] == "00000"
tags = revent["tags"]
r1_tag = [tag for tag in tags if tag[0] == "r1"]
assert len(r1_tag) == 1
assert r1_tag[0][1] == "v1"
e_tag = [tag for tag in tags if tag[0] == "e"]
assert len(e_tag) == 1
assert e_tag[0][1] == event["id"]
p_tag = [tag for tag in tags if tag[0] == "p"]
assert len(p_tag) == 1
assert p_tag[0][1] == nwc_service_provider.public_key_hex
@pytest.mark.asyncio
async def test_handle_rejects_same_event_replay(
nwc_service_provider, nwc_service_provider2
):
content = nwc_service_provider._json_dumps(
{"method": "pay_invoice", "params": {"invoice": "abc"}}
)
content = nwc_service_provider.private_key.encrypt_message(
content, nwc_service_provider2.public_key_hex
)
event = {
"kind": 23194,
"content": content,
"tags": [["p", nwc_service_provider2.public_key_hex]],
"created_at": int(time.time()),
}
signed = nwc_service_provider._sign_event(event)
calls = 0
async def _handle_pay_invoice(provider, pubkey, content):
nonlocal calls
calls += 1
return [({"preimage": "00000"}, None, [])]
async def _send_pass(obj):
pass
nwc_service_provider2._send = _send_pass
nwc_service_provider2._create_subscription()
nwc_service_provider2.add_request_listener("pay_invoice", _handle_pay_invoice)
await nwc_service_provider2._handle_request(signed)
with pytest.raises(Exception, match="already handled"):
await nwc_service_provider2._handle_request(signed)
assert calls == 1
@pytest.mark.asyncio
async def test_relay_dispatches_requests_without_waiting_for_previous_request(
nwc_service_provider, monkeypatch
):
sub = nwc_service_provider._create_subscription()
sub.requests_sub_id = "requests"
sub.requests_eose = True
sub.responses_eose = True
monkeypatch.setattr(nwc_service_provider, "_verify_event", lambda event: True)
first_request_finished = asyncio.Event()
second_request_finished = asyncio.Event()
async def _handle_request(event):
if event["id"] == "first":
await first_request_finished.wait()
else:
second_request_finished.set()
return []
monkeypatch.setattr(nwc_service_provider, "_handle_request", _handle_request)
def request(event_id):
return json.dumps(
[
"EVENT",
sub.requests_sub_id,
{
"id": event_id,
"kind": 23194,
"pubkey": "a" * 64,
"content": "",
"tags": [["p", nwc_service_provider.public_key_hex]],
"created_at": int(time.time()),
},
]
)
await nwc_service_provider._on_message(None, request("first"))
await nwc_service_provider._on_message(None, request("second"))
await asyncio.wait_for(second_request_finished.wait(), timeout=1)
assert not first_request_finished.is_set()
first_request_finished.set()
await asyncio.gather(*list(nwc_service_provider.request_tasks))
@pytest.mark.asyncio
async def test_cleanup_cancels_pending_request_tasks(nwc_service_provider, monkeypatch):
request_started = asyncio.Event()
async def _handle_request(event):
request_started.set()
await asyncio.Event().wait()
return []
monkeypatch.setattr(nwc_service_provider, "_handle_request", _handle_request)
nwc_service_provider._dispatch_request({"id": "pending"})
await request_started.wait()
await nwc_service_provider.cleanup()
assert not nwc_service_provider.request_tasks
@pytest.mark.asyncio
async def test_send_info_event(nwc_service_provider):
"""_send_info_event should publish a signed kind-13194 event."""
nwc_service_provider.add_request_listener(
"pay_invoice", lambda *args, **kwargs: None # type: ignore[arg-type]
)
sent: list[list] = []
async def _send_capture(obj):
sent.append(obj)
nwc_service_provider._send = _send_capture
nwc_service_provider.connected = True
await nwc_service_provider._send_info_event()
assert len(sent) == 1
msg = sent[0]
assert msg[0] == "EVENT"
event = msg[1]
assert event["kind"] == 13194
assert "pay_invoice" in event["content"]
assert nwc_service_provider._verify_event(event)
@pytest.mark.asyncio
async def test_info_event_loop_resends(nwc_service_provider):
"""_info_event_loop should resend the info event while connected."""
sent: list[list] = []
async def _send_capture(obj):
sent.append(obj)
nwc_service_provider._send = _send_capture
nwc_service_provider.connected = True
loop_task = asyncio.create_task(nwc_service_provider._info_event_loop())
# Allow the loop to run through one sleep cycle (patched to near-zero).
# We drive it by cancelling right after the first send opportunity.
await asyncio.sleep(0) # yield to let the task start
# Manually trigger a resend call to verify the helper works correctly.
await nwc_service_provider._send_info_event()
loop_task.cancel()
try:
await loop_task
except asyncio.CancelledError:
pass
# At least the manual call went through.
assert len(sent) >= 1
for msg in sent:
assert msg[0] == "EVENT"
assert msg[1]["kind"] == 13194
@pytest.mark.asyncio
async def test_info_event_loop_skips_when_disconnected(nwc_service_provider):
"""_info_event_loop should not send the info event while disconnected."""
sent: list[list] = []
async def _send_capture(obj):
sent.append(obj)
nwc_service_provider._send = _send_capture
nwc_service_provider.connected = False # not connected
loop_task = asyncio.create_task(nwc_service_provider._info_event_loop())
await asyncio.sleep(0)
loop_task.cancel()
try:
await loop_task
except asyncio.CancelledError:
pass
# Nothing should have been sent because connected=False.
assert len(sent) == 0