#!/usr/bin/env python3 """Upload + conversion test for La Suite Docs. Verifies the end-to-end conversion path: - `.md` upload → backend → y-provider (yjs) - `.docx` upload → backend → docspec (BlockNote JSON) → y-provider (yjs) Exercises CONVERSION_UPLOAD_ENABLED on the backend, backend→y-provider auth (Y_PROVIDER_API_KEY_FILE) and routing (Y_PROVIDER_API_BASE_URL), and DOCSPEC_API_URL for the .docx import path. """ import argparse import io import json import os import sys import urllib.error import urllib.request import uuid import zipfile sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..')) from utils.tests.helpers import ( http_post, load_toml_credentials, resolve_domain, ) def build_minimal_docx() -> bytes: """Build a minimal valid .docx (OOXML) in memory.""" buf = io.BytesIO() with zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as z: z.writestr('[Content_Types].xml', '' '' '' '' '' '') z.writestr('_rels/.rels', '' '' '' '') z.writestr('word/document.xml', '' '' 'Upload conversion test' '') return buf.getvalue() def get_oidc_token(kc_url, creds): """Direct grant flow against the lasuite-docs realm.""" 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": creds["kc_test_user"], "password": creds["kc_test_pass"], "scope": "openid email", }, content_type="application/x-www-form-urlencoded", ) return (data or {}).get("access_token", "") def multipart_upload(url, token, filename, content_type, body): """POST a file via multipart/form-data with a Bearer token.""" boundary = uuid.uuid4().hex parts = [ f"--{boundary}\r\n".encode(), b'Content-Disposition: form-data; name="title"\r\n\r\n', b"upload-conversion-test\r\n", f"--{boundary}\r\n".encode(), f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode(), f"Content-Type: {content_type}\r\n\r\n".encode(), body, f"\r\n--{boundary}--\r\n".encode(), ] req_body = b"".join(parts) req = urllib.request.Request( url, data=req_body, headers={ "Authorization": f"Bearer {token}", "Content-Type": f"multipart/form-data; boundary={boundary}", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=30) as resp: return resp.status, resp.read().decode() except urllib.error.HTTPError as e: return e.code, e.read().decode() def upload_step(label, docs_url, token, filename, content_type, body): print(f"Step: Upload {label} ...") status, raw = multipart_upload( f"{docs_url}/api/v1.0/documents/", token, filename, content_type, body, ) if status != 201: print(f" FAIL: {label} upload returned HTTP {status}: {raw[:200]}") sys.exit(1) try: doc = json.loads(raw) except json.JSONDecodeError: print(f" FAIL: {label} response not JSON: {raw[:200]}") sys.exit(1) doc_id = doc.get('id') if not doc_id: print(f" FAIL: {label} response missing 'id': {raw[:200]}") sys.exit(1) print(f" PASS: {label} converted and persisted (document id {doc_id})") 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) docs_domain = args.domain or resolve_domain('lasuite-docs') kc_domain = resolve_domain('keycloak') docs_url = f"https://{docs_domain}" kc_url = f"https://{kc_domain}" print("Testing file upload + conversion (CONVERSION_UPLOAD_ENABLED + Y_PROVIDER)") print() print("Step 1: Obtaining access token from Keycloak ...") token = get_oidc_token(kc_url, creds) if not token: print(" FAIL: Could not obtain access token from Keycloak") sys.exit(1) print(f" PASS: Obtained access token ({len(token)} chars)") upload_step( ".md file", docs_url, token, "upload-test.md", "text/markdown", b"# Upload Conversion Test\n\nValidating **CONVERSION_UPLOAD_ENABLED** end-to-end.\n", ) upload_step( ".docx file", docs_url, token, "upload-test.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", build_minimal_docx(), ) print() print("PASS: Upload + conversion working end-to-end (.md and .docx)") if __name__ == '__main__': main()