From 90c69477d5f701158112b3c294eff26312f89da6 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 25 Sep 2026 17:10:15 +0200 Subject: feat: invitation links no longer bound to an e-mail address MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A link is redeemable by whoever opens it first, so it can be sent by any messaging app. The address is optional (mail + label only); a link lives 7 days, fixed. Adds a Share button; see MESHBAY_DESIGN.md §3.4. Co-Authored-By: Claude Opus 5.5 --- packages/meshbay-node/tests/test_ops_links.py | 32 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) (limited to 'packages/meshbay-node/tests/test_ops_links.py') diff --git a/packages/meshbay-node/tests/test_ops_links.py b/packages/meshbay-node/tests/test_ops_links.py index 00258d3..9ad01d2 100644 --- a/packages/meshbay-node/tests/test_ops_links.py +++ b/packages/meshbay-node/tests/test_ops_links.py @@ -1,13 +1,15 @@ """ 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. +The CLI makes both halves itself: the node's code, then the hub's ticket, then +the link. The address is optional and only labels 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 +from datetime import UTC, datetime, timedelta import pytest from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -76,12 +78,30 @@ async def test_a_refused_ticket_takes_the_code_back(roster): assert [i for i in await roster.list_invites() if i["kind"] == KIND_LINK] == [] -async def test_an_address_is_required(roster): +async def test_an_address_is_optional_but_must_be_one_when_given(roster): + hub = _Hub() + out = await ops.create_link_invitation(_state(roster, hub), GROUP) + assert LINK.match(out["link"]), out["link"] + assert hub.created == [(GROUP, "", out["invite_id"])] with pytest.raises(ops.OpError) as refused: - await ops.create_link_invitation(_state(roster, _Hub()), GROUP, "alice") + await ops.create_link_invitation(_state(roster, hub), GROUP, "alice") assert refused.value.status == 422 +async def test_a_link_lives_seven_days_whatever_the_invite_setting(roster): + class _Node: + invite_ttl_hours = 24 * 30 + + class _Config: + node = _Node() + + state = _state(roster, _Hub()) + state["config"] = _Config() + out = await ops.create_link_invite(state, GROUP) + left = datetime.fromisoformat(out["expires_at"]) - datetime.now(UTC) + assert timedelta(days=6, hours=23) < left <= timedelta(days=7) + + async def test_cancelling_takes_back_both_halves(roster): hub = _Hub() state = _state(roster, hub) -- cgit v1.2.3