move all mocks in a top level mock folder
This commit is contained in:
2
mocks/mock-google-oidc/.gitignore
vendored
Normal file
2
mocks/mock-google-oidc/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
73
mocks/mock-google-oidc/README.md
Normal file
73
mocks/mock-google-oidc/README.md
Normal 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".
|
||||
12
mocks/mock-google-oidc/package.json
Normal file
12
mocks/mock-google-oidc/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
77
mocks/mock-google-oidc/server.mjs
Normal file
77
mocks/mock-google-oidc/server.mjs
Normal 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
266
mocks/mock_synapse.py
Normal 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.")
|
||||
Reference in New Issue
Block a user