Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion synapse_room_code/get_inviter_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
USERS_DEFAULT_POWER_LEVEL_KEY,
USERS_POWER_LEVEL_KEY,
)
from synapse_room_code.user_is_room_member import user_is_room_member


async def get_inviter_user(api: ModuleApi, room_id: str) -> Optional[UserID]:
Expand Down Expand Up @@ -57,7 +58,7 @@ async def get_inviter_user(api: ModuleApi, room_id: str) -> Optional[UserID]:
if not isinstance(users_power_level, dict):
users_power_level = {}

# Find the user with the highest power level
# Find the user with the highest power level that is still a member of the room
local_user_id_with_highest_power = None
highest_local_power = users_default
for user_id, power_level in users_power_level.items():
Expand All @@ -71,6 +72,11 @@ async def get_inviter_user(api: ModuleApi, room_id: str) -> Optional[UserID]:
if not isinstance(user_id, str):
continue

# ensure user is a member of the room
is_member = await user_is_room_member(api=api, user_id=user_id, room_id=room_id)
if not is_member:
continue

if power_level > highest_local_power and api.is_mine(user_id):
highest_local_power = power_level
local_user_id_with_highest_power = user_id
Expand Down
183 changes: 183 additions & 0 deletions tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,59 @@ async def wait_for_room_invitation(
total_wait_time += wait_interval
return received_invitation

async def set_room_power_levels(
self, room_id: str, access_token: str, user_power_levels: dict
):
headers = {"Authorization": f"Bearer {access_token}"}
set_power_levels_url = f"http://localhost:8008/_matrix/client/v3/rooms/{room_id}/state/m.room.power_levels"
power_levels_content = {
"users": user_power_levels,
"users_default": 0,
"events": {},
"events_default": 0,
"state_default": 50,
"ban": 50,
"kick": 50,
"redact": 50,
"invite": 50,
}
response = requests.put(
set_power_levels_url,
json=power_levels_content,
headers=headers,
)
self.assertEqual(response.status_code, 200)
event_id = response.json()["event_id"]
self.assertIsInstance(event_id, str)
return event_id

async def join_room(self, room_id: str, access_token: str):
headers = {"Authorization": f"Bearer {access_token}"}
join_room_url = f"http://localhost:8008/_matrix/client/v3/rooms/{room_id}/join"
response = requests.post(join_room_url, json={}, headers=headers)
self.assertEqual(response.status_code, 200)
room_id_response = response.json()["room_id"]
self.assertIsInstance(room_id_response, str)
return room_id_response

async def invite_user_to_room(self, room_id: str, user_id: str, access_token: str):
headers = {"Authorization": f"Bearer {access_token}"}
invite_user_url = (
f"http://localhost:8008/_matrix/client/v3/rooms/{room_id}/invite"
)
response = requests.post(
invite_user_url, json={"user_id": user_id}, headers=headers
)
self.assertEqual(response.status_code, 200)

async def leave_room(self, room_id: str, access_token: str):
headers = {"Authorization": f"Bearer {access_token}"}
leave_room_url = (
f"http://localhost:8008/_matrix/client/v3/rooms/{room_id}/leave"
)
response = requests.post(leave_room_url, json={}, headers=headers)
self.assertEqual(response.status_code, 200)

async def start_test_postgres(self):
postgresql = None
try:
Expand Down Expand Up @@ -456,6 +509,136 @@ async def test_e2e_knock_with_code_sqlite(self) -> None:
shutil.rmtree(synapse_dir)
raise e

async def _test_knock_with_code_admin_left_common(
self,
db: Literal["sqlite", "postgresql"] = "sqlite",
postgresql_url: Union[str, None] = None,
) -> None:
"""
Common test logic for testing knock with code when an admin with high power level has left the room.
Tests that the system can still invite users through other remaining room members with sufficient power.
"""
postgres = None
synapse_dir = None
server_process = None
stdout_thread = None
stderr_thread = None

try:
access_code = "vldcde1"

# Start database if needed
if db == "postgresql":
postgres, postgresql_url = await self.start_test_postgres()

# Start Synapse server
(
synapse_dir,
config_path,
server_process,
stdout_thread,
stderr_thread,
) = await self.start_test_synapse(db=db, postgresql_url=postgresql_url)

# Register test users
await self.register_user(
config_path=config_path,
dir=synapse_dir,
user="test1",
password="123123123",
admin=True,
)
await self.register_user(
config_path=config_path,
dir=synapse_dir,
user="test2",
password="123123123",
admin=True,
)
await self.register_user(
config_path=config_path,
dir=synapse_dir,
user="test3",
password="123123123",
admin=True,
)

# Login to obtain access tokens
user_1_id, user_1_access_token = await self.login_user(
user="test1", password="123123123"
)
user_2_id, user_2_access_token = await self.login_user(
user="test2", password="123123123"
)
user_3_id, user_3_access_token = await self.login_user(
user="test3", password="123123123"
)

# Create room and set up the scenario
room_id = await self.create_private_room(user_1_access_token)

# User 2 needs to be invited and then join the room first (required before they can leave)
await self.invite_user_to_room(
room_id=room_id, user_id=user_2_id, access_token=user_1_access_token
)
await self.join_room(room_id=room_id, access_token=user_2_access_token)

# Set power levels: user1 = 100 (room creator), user2 = 100, user3 = 0
await self.set_room_power_levels(
room_id=room_id,
access_token=user_1_access_token,
user_power_levels={
user_1_id: 100,
user_2_id: 100,
},
)

# User 2 (with highest power level besides creator) leaves the room
await self.leave_room(room_id=room_id, access_token=user_2_access_token)

# Set room to be knockable with access code
await self.set_room_knockable_with_code(
room_id=room_id,
access_token=user_1_access_token,
access_code=access_code,
)

# Test the knock with code functionality - should still work because user1 is still in the room
await self.knock_with_code(access_code, user_3_access_token)

# Wait for the invite - should work because user1 is still available to invite
received_invitation = await self.wait_for_room_invitation(
room_id=room_id,
user_id=user_3_id,
access_token=user_1_access_token,
)
if not received_invitation:
self.fail("User 3 was not invited to the room")
else:
logger.info(
"User 3 was invited to the room successfully after admin left"
)

finally:
# Clean up resources
if postgres is not None:
postgres.stop()
if server_process is not None:
server_process.terminate()
server_process.wait()
if stdout_thread is not None:
stdout_thread.join()
if stderr_thread is not None:
stderr_thread.join()
if synapse_dir is not None:
shutil.rmtree(synapse_dir)

async def test_e2e_knock_with_code_admin_left_sqlite(self) -> None:
await self._test_knock_with_code_admin_left_common(db="sqlite")

async def test_e2e_knock_with_code_admin_left_postgresql(self) -> None:
await self._test_knock_with_code_admin_left_common(db="postgresql")

async def test_e2e_knock_with_code_postgresql(self) -> None:
postgres = None
server_process = None
Expand Down