Merge pull request #3 from p-num/chore/move-mock-to-separate-folder
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