move all mocks in a top level mock folder

This commit is contained in:
Letro Bot
2026-04-07 14:10:42 +03:30
parent f4533ade28
commit 0ef47fd90e
5 changed files with 430 additions and 0 deletions

2
mocks/mock-google-oidc/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
package-lock.json

View File

@@ -0,0 +1,73 @@
# Mock Google OIDC Provider
A lightweight mock OpenID Connect provider that simulates Google login for local MAS development. It lets you test the upstream OAuth2 / "Sign in with Google" flow without needing real Google credentials.
## Prerequisites
- Node.js 18+
- MAS configured with an upstream OAuth2 provider pointing to this mock
## Quick start
```sh
cd mock-google-oidc
npm install
npm start
```
The server starts at `http://localhost:5556`.
## How it works
The mock uses [`oidc-provider`](https://github.com/panva/node-oidc-provider) to run a standards-compliant OIDC server with dev interactions enabled. When MAS redirects to the mock for login, you'll see a simple form where you can enter any email-like value (e.g. `taylor@gmail.com`). The mock will return that as the authenticated user with synthetic profile claims.
## MAS configuration
Add an upstream OAuth2 provider entry in your MAS `config.yaml`:
```yaml
upstream_oauth2:
providers:
- id: "01JQ0FAKEG00G1E0D1CPR0V1D3"
human_name: "Google"
issuer: "http://localhost:5556"
client_id: "mas-dev"
client_secret: "mas-dev-secret"
token_endpoint_auth_method: "client_secret_post"
scope: "openid email profile"
claims_imports:
localpart:
action: suggest
template: "{{ user.preferred_username }}"
displayname:
action: suggest
template: "{{ user.name }}"
email:
action: suggest
template: "{{ user.email }}"
set_email_verification: always
```
## Environment variables
| Variable | Default | Description |
|----------|---------|-------------|
| `MOCK_OIDC_ISSUER` | `http://localhost:5556` | The issuer URL |
| `MAS_PROVIDER_ID` | `01JQ0FAKEG00G1E0D1CPR0V1D3` | The upstream provider ID configured in MAS |
| `MOCK_OIDC_CLIENT_ID` | `mas-dev` | OAuth2 client ID |
| `MOCK_OIDC_CLIENT_SECRET` | `mas-dev-secret` | OAuth2 client secret |
## Running with the full dev stack
```sh
# Terminal 1: Mock Synapse (handles homeserver API calls)
python3 mock_synapse.py
# Terminal 2: Mock Google OIDC
cd mock-google-oidc && npm start
# Terminal 3: MAS
cargo run -- server -c config.yaml
```
Then visit `http://[::]:8080/login` and click "Sign in with Google".

View File

@@ -0,0 +1,12 @@
{
"name": "mock-google-oidc",
"private": true,
"type": "module",
"scripts": {
"start": "node server.mjs"
},
"dependencies": {
"jose": "^6.0.11",
"oidc-provider": "^9.5.1"
}
}

View File

@@ -0,0 +1,77 @@
import { exportJWK, generateKeyPair } from 'jose';
import { Provider } from 'oidc-provider';
const ISSUER = process.env.MOCK_OIDC_ISSUER ?? 'http://localhost:5556';
const PROVIDER_ID =
process.env.MAS_PROVIDER_ID ?? '01JQ0FAKEG00G1E0D1CPR0V1D3';
const CLIENT_ID = process.env.MOCK_OIDC_CLIENT_ID ?? 'mas-dev';
const CLIENT_SECRET =
process.env.MOCK_OIDC_CLIENT_SECRET ?? 'mas-dev-secret';
const redirectUris = [
`http://localhost:8080/upstream/callback/${PROVIDER_ID}`,
`http://127.0.0.1:8080/upstream/callback/${PROVIDER_ID}`,
`http://[::]:8080/upstream/callback/${PROVIDER_ID}`,
];
const { privateKey } = await generateKeyPair('RS256', { extractable: true });
const jwk = await exportJWK(privateKey);
jwk.use = 'sig';
jwk.alg = 'RS256';
jwk.kid = 'mock-google-rs256';
const configuration = {
clients: [
{
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uris: redirectUris,
response_types: ['code'],
grant_types: ['authorization_code'],
token_endpoint_auth_method: 'client_secret_post',
},
],
jwks: { keys: [jwk] },
claims: {
openid: ['sub'],
email: ['email', 'email_verified'],
profile: ['name', 'preferred_username', 'picture'],
},
cookies: {
keys: ['mock-google-oidc-dev-key-1', 'mock-google-oidc-dev-key-2'],
},
features: {
devInteractions: { enabled: true },
rpInitiatedLogout: { enabled: false },
},
async findAccount(_ctx, sub) {
const email = sub.includes('@') ? sub : `${sub}@gmail.com`;
const preferredUsername = email.split('@')[0];
return {
accountId: sub,
async claims(_use, _scope) {
return {
sub,
email,
email_verified: true,
name: 'Taylor Google User',
preferred_username: preferredUsername,
picture:
'https://www.gstatic.com/images/branding/product/1x/avatar_circle_blue_512dp.png',
};
},
};
},
};
const provider = new Provider(ISSUER, configuration);
provider.listen(5556, () => {
console.log(`Mock Google OIDC running at ${ISSUER}`);
console.log('Configured redirect URIs:');
for (const uri of redirectUris) {
console.log(` - ${uri}`);
}
console.log('Use any email-like value in the dev interaction login form.');
});

266
mocks/mock_synapse.py Normal file
View File

@@ -0,0 +1,266 @@
#!/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 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.")