#!/usr/bin/env python3 """Meeting flow test for La Suite Meet — create room, join as two users, clean up.""" import argparse import base64 import json import os import sys import time import urllib.error import urllib.request sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) from utils.tests.helpers import ( http_get, http_post, load_toml_credentials, resolve_domain, ) def get_meet_token(kc_url, creds, username, password): """Obtain a Keycloak token for the given user.""" status, data = http_post( f"{kc_url}/realms/{creds['kc_realm']}/protocol/openid-connect/token", data={ "grant_type": "password", "client_id": creds["kc_client_id"], "client_secret": creds["kc_client_secret"], "username": username, "password": password, "scope": "openid email", }, content_type="application/x-www-form-urlencoded", ) return (data or {}).get("access_token", "") def decode_jwt_payload(token): """Decode the payload segment of a JWT token.""" try: payload_b64 = token.split('.')[1] # Add padding payload_b64 += '=' * (4 - len(payload_b64) % 4) # Replace URL-safe chars payload_b64 = payload_b64.replace('-', '+').replace('_', '/') decoded = base64.b64decode(payload_b64) return json.loads(decoded) except Exception: return {} def main(): parser = argparse.ArgumentParser() parser.add_argument('--domain', default=os.environ.get('TEST_DOMAIN')) args = parser.parse_args() recipe_dir = os.path.join(os.path.dirname(__file__), '..') creds = load_toml_credentials(recipe_dir, 'keycloak') if creds is None: print("FAIL: Credentials file not found: keycloak-test-credentials..toml") print("Run setup_keycloak_integration.py first.") sys.exit(1) meet_domain = args.domain or resolve_domain('lasuite-meet') kc_domain = resolve_domain('keycloak') meet_url = f"https://{meet_domain}" kc_url = f"https://{kc_domain}" room_name = f"autotest-{int(time.time())}" print("Testing meeting flow: create room, join as two users, clean up") print() # Step 1: Authenticate both users via Keycloak print("Step 1: Authenticating both users via Keycloak ...") token_user1 = get_meet_token(kc_url, creds, creds["kc_test_user"], creds["kc_test_pass"]) if not token_user1: print(f" FAIL: Could not authenticate {creds['kc_test_user']}") sys.exit(1) print(f" PASS: {creds['kc_test_user']} authenticated ({len(token_user1)} chars)") token_user2 = get_meet_token(kc_url, creds, creds["kc_test_user2"], creds["kc_test_pass2"]) if not token_user2: print(f" FAIL: Could not authenticate {creds['kc_test_user2']}") sys.exit(1) print(f" PASS: {creds['kc_test_user2']} authenticated ({len(token_user2)} chars)") # Step 2: User 1 creates a room print(f"Step 2: User 1 creates room '{room_name}' ...") status, create_body = http_post( f"{meet_url}/api/v1.0/rooms/", data={"name": room_name, "access_level": "public"}, headers={"Authorization": f"Bearer {token_user1}"}, ) if status != 201: print(f" FAIL: Room creation returned HTTP {status} (expected 201)") sys.exit(1) room_id = create_body["id"] room_slug = create_body["slug"] user1_lk_room = create_body["livekit"]["room"] user1_lk_token = create_body["livekit"]["token"] if not room_id or not user1_lk_token: print(" FAIL: Room created but missing id or LiveKit token") sys.exit(1) print(f" PASS: Room created (id={room_id}, slug={room_slug})") print(f" PASS: User 1 received LiveKit token (room={user1_lk_room})") # Step 3: User 2 joins the room print(f"Step 3: User 2 joins room '{room_slug}' ...") status, join_body = http_get( f"{meet_url}/api/v1.0/rooms/{room_id}/", headers={"Authorization": f"Bearer {token_user2}"}, ) if status != 200: print(f" FAIL: Room retrieval returned HTTP {status} (expected 200)") # Clean up req = urllib.request.Request(f"{meet_url}/api/v1.0/rooms/{room_id}/", method="DELETE") req.add_header("Authorization", f"Bearer {token_user1}") try: urllib.request.urlopen(req, timeout=10) except Exception: pass sys.exit(1) user2_lk_room = (join_body or {}).get("livekit", {}).get("room", "") user2_lk_token = (join_body or {}).get("livekit", {}).get("token", "") if not user2_lk_token: print(" FAIL: User 2 did not receive a LiveKit token") # Clean up req = urllib.request.Request(f"{meet_url}/api/v1.0/rooms/{room_id}/", method="DELETE") req.add_header("Authorization", f"Bearer {token_user1}") try: urllib.request.urlopen(req, timeout=10) except Exception: pass sys.exit(1) print(f" PASS: User 2 received LiveKit token (room={user2_lk_room})") # Step 4: Verify both tokens reference the same room print("Step 4: Verifying LiveKit tokens ...") if user1_lk_room != user2_lk_room: print(f" FAIL: LiveKit room mismatch: user1={user1_lk_room} user2={user2_lk_room}") sys.exit(1) print(f" PASS: Both users have tokens for the same LiveKit room ({user1_lk_room})") # Decode JWT payloads and verify distinct identities user1_payload = decode_jwt_payload(user1_lk_token) user2_payload = decode_jwt_payload(user2_lk_token) user1_identity = user1_payload.get("sub", "") user2_identity = user2_payload.get("sub", "") if user1_identity and user2_identity and user1_identity != user2_identity: print(f" PASS: Tokens have distinct identities (user1={user1_identity[:12]}..., user2={user2_identity[:12]}...)") else: print(" WARN: Could not verify distinct JWT identities (non-fatal)") # Step 5: User 1 deletes the room print("Step 5: User 1 deletes the room ...") req = urllib.request.Request(f"{meet_url}/api/v1.0/rooms/{room_id}/", method="DELETE") req.add_header("Authorization", f"Bearer {token_user1}") try: with urllib.request.urlopen(req, timeout=10) as resp: delete_status = resp.getcode() except urllib.error.HTTPError as e: delete_status = e.code if delete_status != 204: print(f" FAIL: Room deletion returned HTTP {delete_status} (expected 204)") sys.exit(1) print(" PASS: Room deleted (HTTP 204)") # Step 6: Verify room is gone print("Step 6: Verifying room no longer exists ...") status, _ = http_get( f"{meet_url}/api/v1.0/rooms/{room_id}/", headers={"Authorization": f"Bearer {token_user1}"}, ) if status == 404: print(" PASS: Room returns 404 (deleted)") elif status == 200: print(" PASS: Room ID no longer resolves to original room") else: print(f" WARN: Unexpected HTTP {status} when checking deleted room (non-fatal)") print() print("PASS: Meeting flow test passed — two users can create, join, and clean up rooms") if __name__ == '__main__': main()