aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/test_availability_between_members.py67
1 files changed, 67 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_availability_between_members.py b/packages/meshbay-hub/tests/test_availability_between_members.py
index d1a4dcb..2be7c03 100644
--- a/packages/meshbay-hub/tests/test_availability_between_members.py
+++ b/packages/meshbay-hub/tests/test_availability_between_members.py
@@ -718,3 +718,70 @@ async def test_a_private_groups_node_list_is_for_its_members(client):
finally:
rev._connected_nodes.pop(node_id, None)
rev._node_groups.pop(node_id, None)
+
+
+async def _announce_key(client, user: dict, sk) -> int:
+ """Announce a *distinct* node key, and return the status code."""
+ from meshbay_common.crypto import pk_to_b64
+
+ pk = pk_to_b64(sk.public_key())
+ ts = int(time.time())
+ msg = f"meshbay:node_announce:{user['user_id']}:{pk}:{ts}".encode()
+ r = await client.post("/v1/nodes/announce", json={
+ "pk_node": pk, "endpoint_hint": "test", "timestamp": ts,
+ "signature": base64.b64encode(sk.sign(msg)).decode(),
+ }, headers={"Authorization": f"Bearer {user['token']}"})
+ return r.status_code
+
+
+async def test_one_account_cannot_announce_unlimited_nodes(client, monkeypatch):
+ """
+ Each new node key is a row in `nodes` and a row in the IP log, and the IP log
+ is kept for a year. Proof of possession (M8) settles *whose* key it is and
+ says nothing about how many: an account in a loop wrote a year of storage on
+ the operator's disk having paid only for signatures.
+
+ Two accounts, because the ceiling has to be per account. One that is shared
+ would let a single member deny every other member the ability to bring a
+ machine online, which is the same defect with better manners.
+ """
+ from meshbay_hub.api import nodes as nodes_api
+
+ monkeypatch.setattr(nodes_api, "MAX_NODES_PER_ACCOUNT", 3)
+ alice = await _make_user(client, "av_nodecap_alice")
+ bob = await _make_user(client, "av_nodecap_bob")
+
+ keys = [Ed25519PrivateKey.generate() for _ in range(4)]
+ for sk in keys[:3]:
+ assert await _announce_key(client, alice, sk) == 201
+
+ assert await _announce_key(client, alice, keys[3]) == 409, (
+ "an account announced past the ceiling")
+
+ # Bob has announced nothing and must be unaffected.
+ assert await _announce_key(client, bob, Ed25519PrivateKey.generate()) == 201, (
+ "one account's ceiling was charged to another's"
+ )
+
+
+async def test_a_node_at_the_ceiling_can_still_refresh_its_address(client, monkeypatch):
+ """
+ The ceiling counts rows, so it must be checked only where a row is added.
+ Applied to every announce, it would freeze the address of every node an
+ account already runs the moment it reached the limit — and a node that
+ cannot re-announce is a node nobody can reach after their ISP renumbers
+ them, which is an outage caused by the protection.
+ """
+ from meshbay_hub.api import nodes as nodes_api
+
+ monkeypatch.setattr(nodes_api, "MAX_NODES_PER_ACCOUNT", 2)
+ alice = await _make_user(client, "av_nodecap_refresh")
+
+ keys = [Ed25519PrivateKey.generate() for _ in range(2)]
+ for sk in keys:
+ assert await _announce_key(client, alice, sk) == 201
+ assert await _announce_key(client, alice, Ed25519PrivateKey.generate()) == 409
+
+ for sk in keys:
+ assert await _announce_key(client, alice, sk) == 201, (
+ "a node already known could not re-announce at the ceiling")