"""lasuite-docs — Q2 SSO-flow acceptance test (Phase 2 §6 Q2 gate). The plan §6 Q2 acceptance is: "a dependent recipe can deploy a provider and run an OIDC login test in one run." This test exercises that contract end-to-end: 1. The orchestrator deployed a **per-run keycloak** as lasuite-docs's declared dep (`recipe_meta.DEPS = ["keycloak"]`). Its domain is in `deps_apps["keycloak"]`. 2. Use the shared SSO-setup harness primitive (`runner/harness/sso.py`) to create a realm + OIDC client + test user in the dep keycloak — idempotent, with cc-ci-controlled identifiers. 3. Exercise the OIDC discovery endpoint (`.well-known/openid-configuration`); assert issuer is the per-run keycloak's `/realms/`. 4. Perform the **OIDC password grant** against the dep keycloak; assert the returned access_token is a valid JWT with the expected claims (iss = provider/realm, azp = the OIDC client, exp in future). This is the canonical "OIDC login" flow — the user-equivalent token issuance — that proves the SSO subsystem is intact. Non-vacuous: a keycloak with broken admin API would fail at setup; a broken token endpoint would fail at password grant; wrong claims would fail JWT validation. Each step uses real credentials generated for THIS run (class-B per §4.4-B); destroyed when the dep is torn down at run end. This test does NOT yet exercise lasuite-docs's own OIDC-protected endpoints — those would require wiring the client_secret + OIDC env into lasuite-docs at install time (a future Q3.1 task; see DECISIONS.md). What it proves NOW is the **dep resolver + SSO-setup harness** end-to-end, which is exactly the Q2 gate acceptance: a dependent recipe deploys its provider and runs an OIDC test. """ from __future__ import annotations import base64 import json import os import sys import time sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "runner")) from harness import sso # noqa: E402 def _b64url_decode(seg: str) -> bytes: pad = "=" * ((4 - len(seg) % 4) % 4) return base64.urlsafe_b64decode(seg + pad) def test_oidc_password_grant_against_dep_keycloak(live_app, deps_apps): """End-to-end: keycloak dep is up → set up realm/client/user → OIDC password grant → JWT.""" assert "keycloak" in deps_apps, ( f"keycloak dep not deployed; deps_apps={deps_apps}. Q2.3 dep resolver did not run." ) kc_domain = deps_apps["keycloak"] creds = sso.setup_keycloak_realm( kc_domain, realm="lasuite-docs", client_id="docs", redirect_uris=[f"https://{live_app}/*"], web_origins=[f"https://{live_app}"], ) # Sanity-check the creds shape assert creds["provider"] == "keycloak" assert creds["provider_domain"] == kc_domain assert creds["realm"] == "lasuite-docs" assert creds["client_id"] == "docs" assert isinstance(creds["client_secret"], str) and len(creds["client_secret"]) >= 16 assert isinstance(creds["password"], str) and len(creds["password"]) >= 16 # OIDC discovery endpoint advertises the realm discovery = sso.assert_discovery_endpoint(creds) expected_iss = f"https://{kc_domain}/realms/lasuite-docs" assert discovery.get("issuer") == expected_iss assert discovery.get("token_endpoint", "").startswith(expected_iss + "/") assert discovery.get("authorization_endpoint", "").startswith(expected_iss + "/") # Password grant flow → real JWT token = sso.oidc_password_grant(creds) assert isinstance(token, str) and token.count(".") == 2, ( f"access_token is not a JWT: {token!r}" ) payload = json.loads(_b64url_decode(token.split(".")[1])) assert payload.get("iss") == expected_iss, f"JWT iss={payload.get('iss')!r} != {expected_iss!r}" assert payload.get("azp") == "docs", f"JWT azp={payload.get('azp')!r} != 'docs'" assert payload.get("typ") == "Bearer", f"JWT typ={payload.get('typ')!r} != 'Bearer'" exp = payload.get("exp") assert isinstance(exp, int) and exp > time.time(), ( f"JWT exp={exp!r} not a future timestamp (now={time.time():.0f})" )