1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
|
"""
Removing someone from a group.
The distinction these pin is the one that matters: removing a member from a
group is not deleting their account. It takes away one membership row, and
leaves the person, their other groups and everything they have uploaded exactly
where they were.
"""
import base64
import hashlib
import pytest
from sqlalchemy import select
from meshbay_hub.db.models import Group, GroupMember, User
def _auth_key(password: str, username: str) -> str:
salt = hashlib.sha256(f"meshbay:auth:v1:{username}".encode()).digest()
return base64.b64encode(
hashlib.pbkdf2_hmac("sha512", password.encode(), salt, 600_000, 32)).decode()
async def _user(client, username, password="a-long-enough-passphrase"):
await client.post("/v1/users/register", json={
"username": username, "email": f"{username}@example.com",
"auth_key": _auth_key(password, username)})
r = await client.post("/v1/users/login", json={
"username": username, "auth_key": _auth_key(password, username)})
return {"Authorization": f"Bearer {r.json()['access_token']}"}
async def _group_with_member(client, owner, member_name, name="crew"):
g = await client.post("/v1/groups", json={"name": name}, headers=owner)
gid = g.json()["group_id"]
await client.post(f"/v1/groups/{gid}/members/{member_name}", json={},
headers=owner)
return gid
@pytest.mark.asyncio
async def test_the_owner_removes_a_member(client, db_session):
owner = await _user(client, "chief_test")
await _user(client, "hanger_on")
gid = await _group_with_member(client, owner, "hanger_on")
r = await client.delete(f"/v1/groups/{gid}/members/hanger_on", headers=owner)
assert r.status_code == 200, r.text
rows = (await db_session.execute(
select(GroupMember).where(GroupMember.group_id == gid))).scalars().all()
assert [m.user_id for m in rows] != [], "the owner lost their own membership"
names = {(await db_session.get(User, m.user_id)).username for m in rows}
assert names == {"chief_test"}
@pytest.mark.asyncio
async def test_removing_a_member_is_not_deleting_an_account(client, db_session):
"""
The account survives untouched, with its other groups. Anything else would
make one group's owner able to erase someone from the whole hub.
"""
owner = await _user(client, "boss_test")
member = await _user(client, "member_x")
elsewhere = await _user(client, "other_owner")
gid = await _group_with_member(client, owner, "member_x")
other = await _group_with_member(client, elsewhere, "member_x", name="elsewhere")
await client.delete(f"/v1/groups/{gid}/members/member_x", headers=owner)
user = (await db_session.execute(
select(User).where(User.username == "member_x"))).scalar_one()
assert user.status == "active", "the account was touched"
me = await client.get("/v1/users/me", headers=member)
assert me.status_code == 200, "they can no longer sign in"
still = await db_session.get(GroupMember, (other, user.id))
assert still is not None, "removing them from one group emptied another"
@pytest.mark.asyncio
async def test_a_member_cannot_remove_anyone(client):
owner = await _user(client, "owner_y_test")
member = await _user(client, "member_y")
await _user(client, "victim_y")
gid = await _group_with_member(client, owner, "member_y")
await client.post(f"/v1/groups/{gid}/members/victim_y", json={}, headers=owner)
r = await client.delete(f"/v1/groups/{gid}/members/victim_y", headers=member)
assert r.status_code == 403
@pytest.mark.asyncio
async def test_the_owner_cannot_be_removed_from_their_own_group(client):
"""Otherwise the group is left with nobody who can invite or remove."""
owner = await _user(client, "owner_z_test")
gid = await _group_with_member(client, owner, "owner_z_test")
r = await client.delete(f"/v1/groups/{gid}/members/owner_z_test", headers=owner)
assert r.status_code == 409
@pytest.mark.asyncio
async def test_removing_someone_who_is_not_a_member_says_so(client):
owner = await _user(client, "owner_w_test")
await _user(client, "stranger")
g = await client.post("/v1/groups", json={"name": "closed"}, headers=owner)
gid = g.json()["group_id"]
r = await client.delete(f"/v1/groups/{gid}/members/stranger", headers=owner)
assert r.status_code == 404
|