summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_ops_links.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_ops_links.py')
-rw-r--r--packages/meshbay-node/tests/test_ops_links.py107
1 files changed, 107 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_ops_links.py b/packages/meshbay-node/tests/test_ops_links.py
new file mode 100644
index 0000000..00258d3
--- /dev/null
+++ b/packages/meshbay-node/tests/test_ops_links.py
@@ -0,0 +1,107 @@
+"""
+Invitation links from the operator's own machine (`member invite --link`).
+
+The CLI makes both halves itself: the node's code, then the hub's ticket bound
+to an address, then the link. What can go wrong is a half left behind — a code
+the hub never ticketed, occupying one of the group's places, or a ticket whose
+code was cancelled — and the CLI asking the hub to mail, which it may not.
+"""
+
+import re
+
+import pytest
+from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+from meshbay_node import ops
+from meshbay_node.roster import KIND_LINK, Roster
+
+GROUP = "0f8fad5b-d9cb-469f-a165-70867728950e"
+LINK = re.compile(
+ r"^https://hub\.example/#/invite\?v=1&g=" + GROUP
+ + r"&t=[A-Za-z0-9_-]{22}&n=[A-Za-z0-9_-]{43}&c=[0-9A-Z]{4}-[0-9A-Z]{4}$")
+
+
+class _Hub:
+ hub_url = "https://hub.example/some/path"
+ _session = True
+
+ def __init__(self, refuse=False, unreachable=False):
+ self.refuse, self.unreachable = refuse, unreachable
+ self.created, self.deleted, self.links = [], [], []
+
+ async def create_invite_link(self, group_id, email, expires_at, node_invite_id, **kw):
+ assert not kw, "the CLI asks the hub for nothing else — no mail"
+ if self.refuse:
+ raise RuntimeError("429 too many links")
+ self.created.append((group_id, email, node_invite_id))
+ self.links.append({"link_id": f"L{len(self.links)}", "status": "pending",
+ "node_invite_id": node_invite_id})
+ return {"ticket": "AbCdEfGhIjKlMnOpQr-_12", "expires_at": expires_at}
+
+ async def list_invite_links(self, group_id):
+ if self.unreachable:
+ raise RuntimeError("hub down")
+ return self.links
+
+ async def delete_invite_link(self, group_id, link_id):
+ self.deleted.append(link_id)
+
+
+@pytest.fixture
+async def roster(tmp_path):
+ r = Roster(db_path=tmp_path / "roster.db")
+ await r.open()
+ yield r
+ await r.close()
+
+
+def _state(roster, hub):
+ return {"roster": roster, "groups_ctx": {GROUP: {}}, "hub": hub,
+ "sk_node": Ed25519PrivateKey.generate(), "config": None}
+
+
+async def test_a_whole_link_names_the_code_the_node_holds(roster):
+ hub = _Hub()
+ out = await ops.create_link_invitation(_state(roster, hub), GROUP, " Alice@Example.test ")
+ assert LINK.match(out["link"]), out["link"]
+ assert hub.created == [(GROUP, "Alice@Example.test", out["invite_id"])]
+ code = out["link"].rsplit("&c=", 1)[1]
+ assert await roster.consume_invite(code, "alice", group_id=GROUP)
+
+
+async def test_a_refused_ticket_takes_the_code_back(roster):
+ with pytest.raises(ops.OpError) as refused:
+ await ops.create_link_invitation(_state(roster, _Hub(refuse=True)), GROUP,
+ "alice@example.test")
+ assert refused.value.status == 502
+ assert [i for i in await roster.list_invites() if i["kind"] == KIND_LINK] == []
+
+
+async def test_an_address_is_required(roster):
+ with pytest.raises(ops.OpError) as refused:
+ await ops.create_link_invitation(_state(roster, _Hub()), GROUP, "alice")
+ assert refused.value.status == 422
+
+
+async def test_cancelling_takes_back_both_halves(roster):
+ hub = _Hub()
+ state = _state(roster, hub)
+ out = await ops.create_link_invitation(state, GROUP, "alice@example.test")
+ code = out["link"].rsplit("&c=", 1)[1]
+ done = await ops.cancel_link_invitation(state, GROUP, out["invite_id"])
+ assert done["node"] and done["hub"] and hub.deleted == ["L0"]
+ assert await roster.consume_invite(code, "alice", group_id=GROUP) is None
+
+
+async def test_an_unreachable_hub_does_not_stop_the_node_half(roster):
+ hub = _Hub()
+ state = _state(roster, hub)
+ out = await ops.create_link_invitation(state, GROUP, "alice@example.test")
+ hub.unreachable = True
+ done = await ops.cancel_link_invitation(state, GROUP, out["invite_id"])
+ assert done["node"] is True and done["hub"] is False
+
+
+async def test_an_unknown_link_is_a_refusal_not_a_success(roster):
+ with pytest.raises(ops.OpError) as refused:
+ await ops.cancel_link_invitation(_state(roster, _Hub()), GROUP, "ab" * 16)
+ assert refused.value.status == 404