The previous commit copied mocks to mocks/ but didn't delete the originals. This removes the root-level mock_synapse.py and mock-google-oidc/, updates path references in the mocks/ copies, and adds config.yaml to .gitignore.
267 lines
9.7 KiB
Python
267 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Mock Synapse server for testing MAS UI without a real homeserver.
|
|
|
|
Handles both Modern (_synapse/mas/...) and Legacy (_synapse/admin/...)
|
|
API endpoints that MAS calls on Synapse.
|
|
|
|
Usage:
|
|
python3 mocks/mock_synapse.py
|
|
|
|
Then start MAS normally:
|
|
cargo run -- server -c config.yaml
|
|
"""
|
|
|
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
from urllib.parse import urlparse, parse_qs
|
|
import json
|
|
import sys
|
|
|
|
|
|
# In-memory store of provisioned users: localpart -> user data
|
|
provisioned_users = {}
|
|
|
|
|
|
class MockSynapseHandler(BaseHTTPRequestHandler):
|
|
|
|
def _send_json(self, data, status=200):
|
|
self.send_response(status)
|
|
self.send_header("Content-Type", "application/json")
|
|
self.end_headers()
|
|
self.wfile.write(json.dumps(data).encode())
|
|
|
|
def _read_body(self):
|
|
length = int(self.headers.get("Content-Length", 0))
|
|
if length:
|
|
return json.loads(self.rfile.read(length))
|
|
return {}
|
|
|
|
# ── GET endpoints ──────────────────────────────────────────────
|
|
|
|
def do_GET(self):
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path.rstrip("/")
|
|
qs = parse_qs(parsed.query)
|
|
|
|
# Modern: check if localpart is available
|
|
if path == "/_synapse/mas/is_localpart_available":
|
|
localpart = qs.get("localpart", [""])[0]
|
|
if localpart in provisioned_users:
|
|
print(f" [mock] is_localpart_available: {localpart} → in use")
|
|
self._send_json({"errcode": "M_USER_IN_USE", "error": "User ID already taken"}, 409)
|
|
else:
|
|
print(f" [mock] is_localpart_available: {localpart} → available")
|
|
self._send_json({}) # 200 = available
|
|
return
|
|
|
|
# Modern: query user info
|
|
if path == "/_synapse/mas/query_user":
|
|
localpart = qs.get("localpart", [""])[0]
|
|
if localpart in provisioned_users:
|
|
user = provisioned_users[localpart]
|
|
print(f" [mock] query_user: {localpart} → found")
|
|
self._send_json({
|
|
"user_id": f"@{localpart}:localhost:8008",
|
|
"display_name": user.get("display_name"),
|
|
"avatar_url": user.get("avatar_url"),
|
|
"is_suspended": False,
|
|
"is_deactivated": False,
|
|
})
|
|
else:
|
|
print(f" [mock] query_user: {localpart} → not found (404)")
|
|
self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404)
|
|
return
|
|
|
|
# Legacy: username available
|
|
if path == "/_synapse/admin/v1/username_available":
|
|
username = qs.get("username", [""])[0]
|
|
print(f" [mock] username_available: {username} → available=true")
|
|
self._send_json({"available": True})
|
|
return
|
|
|
|
# Legacy: get user info
|
|
if "/_synapse/admin/v2/users/" in path and "/devices" not in path:
|
|
print(f" [mock] get user → not found (404)")
|
|
self._send_json({"errcode": "M_NOT_FOUND", "error": "User not found"}, 404)
|
|
return
|
|
|
|
# Legacy: list devices
|
|
if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"):
|
|
print(f" [mock] list devices → empty")
|
|
self._send_json({"devices": []})
|
|
return
|
|
|
|
# Matrix client versions
|
|
if path == "/_matrix/client/versions":
|
|
self._send_json({"versions": ["v1.1", "v1.2", "v1.3"]})
|
|
return
|
|
|
|
# Catch-all
|
|
print(f" [mock] Unhandled GET: {self.path}")
|
|
self._send_json({})
|
|
|
|
# ── POST endpoints ─────────────────────────────────────────────
|
|
|
|
def do_POST(self):
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path.rstrip("/")
|
|
body = self._read_body()
|
|
|
|
# Modern: provision user
|
|
if path == "/_synapse/mas/provision_user":
|
|
localpart = body.get("localpart", "?")
|
|
is_new = localpart not in provisioned_users
|
|
provisioned_users[localpart] = {
|
|
"display_name": body.get("set_displayname"),
|
|
"avatar_url": body.get("set_avatar_url"),
|
|
"emails": body.get("set_emails", []),
|
|
}
|
|
status = 201 if is_new else 200
|
|
print(f" [mock] provision_user: {localpart} → {status} {'created' if is_new else 'updated'}")
|
|
self._send_json({}, status)
|
|
return
|
|
|
|
# Modern: upsert device
|
|
if path == "/_synapse/mas/upsert_device":
|
|
print(f" [mock] upsert_device: {body.get('device_id', '?')} → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: update device display name
|
|
if path == "/_synapse/mas/update_device_display_name":
|
|
print(f" [mock] update_device_display_name → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: delete device
|
|
if path == "/_synapse/mas/delete_device":
|
|
print(f" [mock] delete_device → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: sync devices
|
|
if path == "/_synapse/mas/sync_devices":
|
|
print(f" [mock] sync_devices → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: delete user
|
|
if path == "/_synapse/mas/delete_user":
|
|
print(f" [mock] delete_user → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: reactivate user
|
|
if path == "/_synapse/mas/reactivate_user":
|
|
print(f" [mock] reactivate_user → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: set displayname
|
|
if path == "/_synapse/mas/set_displayname":
|
|
localpart = body.get("localpart", "?")
|
|
if localpart in provisioned_users:
|
|
provisioned_users[localpart]["display_name"] = body.get("displayname")
|
|
print(f" [mock] set_displayname: {localpart} → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: unset displayname
|
|
if path == "/_synapse/mas/unset_displayname":
|
|
localpart = body.get("localpart", "?")
|
|
if localpart in provisioned_users:
|
|
provisioned_users[localpart]["display_name"] = None
|
|
print(f" [mock] unset_displayname: {localpart} → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Modern: allow cross-signing reset
|
|
if path == "/_synapse/mas/allow_cross_signing_reset":
|
|
print(f" [mock] allow_cross_signing_reset → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Legacy: create device
|
|
if "/_synapse/admin/v2/users/" in path and path.endswith("/devices"):
|
|
print(f" [mock] legacy create device → 201")
|
|
self._send_json({}, 201)
|
|
return
|
|
|
|
# Legacy: bulk delete devices
|
|
if "/_synapse/admin/v2/users/" in path and "delete_devices" in path:
|
|
print(f" [mock] legacy bulk delete devices → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Legacy: deactivate user
|
|
if "/_synapse/admin/v1/deactivate/" in path:
|
|
print(f" [mock] legacy deactivate → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Legacy: allow cross-signing reset
|
|
if "_allow_cross_signing_replacement_without_uia" in path:
|
|
print(f" [mock] legacy allow cross-signing reset → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Catch-all
|
|
print(f" [mock] Unhandled POST: {path} body={body}")
|
|
self._send_json({})
|
|
|
|
# ── PUT endpoints ──────────────────────────────────────────────
|
|
|
|
def do_PUT(self):
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path.rstrip("/")
|
|
body = self._read_body()
|
|
|
|
# Legacy: create/update user
|
|
if "/_synapse/admin/v2/users/" in path and "/devices/" not in path:
|
|
print(f" [mock] legacy put user → 201 created")
|
|
self._send_json({}, 201)
|
|
return
|
|
|
|
# Legacy: update device display name
|
|
if "/_synapse/admin/v2/users/" in path and "/devices/" in path:
|
|
print(f" [mock] legacy update device display name → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Client API: set displayname
|
|
if "/_matrix/client/v3/profile/" in path and "/displayname" in path:
|
|
print(f" [mock] set profile displayname → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Catch-all
|
|
print(f" [mock] Unhandled PUT: {path} body={body}")
|
|
self._send_json({})
|
|
|
|
# ── DELETE endpoints ───────────────────────────────────────────
|
|
|
|
def do_DELETE(self):
|
|
parsed = urlparse(self.path)
|
|
path = parsed.path.rstrip("/")
|
|
|
|
# Legacy: delete device
|
|
if "/_synapse/admin/v2/users/" in path and "/devices/" in path:
|
|
print(f" [mock] legacy delete device → ok")
|
|
self._send_json({})
|
|
return
|
|
|
|
# Catch-all
|
|
print(f" [mock] Unhandled DELETE: {path}")
|
|
self._send_json({})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8008
|
|
server = HTTPServer(("localhost", port), MockSynapseHandler)
|
|
print(f"Mock Synapse running on http://localhost:{port}")
|
|
print("Press Ctrl+C to stop\n")
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("\nStopped.")
|