Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m57s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
199 lines
9.5 KiB
Python
199 lines
9.5 KiB
Python
"""Real owner portal/adapters/receiver with synthetic session, directory and mail sink."""
|
|
|
|
import html
|
|
import json
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from socketserver import ThreadingMixIn
|
|
from wsgiref.simple_server import WSGIRequestHandler, WSGIServer, make_server
|
|
|
|
import pytest
|
|
|
|
TENANT = "tenant:communication-test"
|
|
CLIENT = "coulomb-social-test"
|
|
TOKEN = "synthetic-user-engine-mail-token"
|
|
|
|
|
|
class QuietHandler(WSGIRequestHandler):
|
|
def log_message(self, *args):
|
|
pass
|
|
|
|
|
|
class FixtureServer(ThreadingMixIn, WSGIServer):
|
|
daemon_threads = True
|
|
|
|
|
|
@contextmanager
|
|
def server(app, ports):
|
|
httpd = make_server("127.0.0.1", 0, app, server_class=FixtureServer,
|
|
handler_class=QuietHandler)
|
|
ports.add(httpd.server_port)
|
|
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
thread.start()
|
|
try:
|
|
yield f"http://127.0.0.1:{httpd.server_port}"
|
|
finally:
|
|
httpd.shutdown()
|
|
httpd.server_close()
|
|
thread.join(timeout=5)
|
|
|
|
|
|
@pytest.mark.browser
|
|
@pytest.mark.usecase("CSOC-UC-010")
|
|
@pytest.mark.usecase("CSOC-UC-011")
|
|
def test_real_owner_registration_and_invitation(tmp_path, monkeypatch):
|
|
ports = set()
|
|
connect, resolve = socket.socket.connect, socket.getaddrinfo
|
|
|
|
def guarded_connect(sock, address):
|
|
if (not isinstance(address, tuple) or address[0] != "127.0.0.1"
|
|
or address[1] not in ports):
|
|
raise RuntimeError("Owner harness refuses non-fixture connections")
|
|
return connect(sock, address)
|
|
|
|
def guarded_resolve(host, port, *args, **kwargs):
|
|
if host != "127.0.0.1" or int(port) not in ports:
|
|
raise RuntimeError("Owner harness refuses DNS/non-fixture connections")
|
|
return resolve(host, port, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(socket.socket, "connect", guarded_connect)
|
|
monkeypatch.setattr(socket.socket, "connect_ex", guarded_connect)
|
|
monkeypatch.setattr(socket, "getaddrinfo", guarded_resolve)
|
|
monkeypatch.setattr(socket, "getfqdn", lambda *args: "localhost")
|
|
with pytest.raises(RuntimeError), socket.socket() as denied:
|
|
denied.connect(("203.0.113.1", 443))
|
|
with pytest.raises(RuntimeError):
|
|
socket.getaddrinfo("provider.example.test", 443)
|
|
|
|
import email_connect
|
|
import user_engine
|
|
from email_connect.callers import CallerPolicy, ScopedTransactionalApplication
|
|
from email_connect.transactional import SQLiteDeliveryStore
|
|
from user_engine.adapters.delivery import HTTPOutboxDeliveryAdapter
|
|
from user_engine.adapters.local import InMemoryUserEngineStore, LocalAuthorizationCheckPort
|
|
from user_engine.adapters.registration_verification import HTTPRegistrationVerificationAdapter
|
|
from user_engine.oidc import BrowserSession, OIDCClient
|
|
from user_engine.ports import ProvisioningResult
|
|
from user_engine.service import UserEngineService
|
|
from user_engine.testing.fixtures import FixtureIdentityClaimsAdapter, human_actor_claims
|
|
from user_engine.web import PortalApplication
|
|
|
|
snapshot = Path(os.environ["PYTHONPATH"]).resolve()
|
|
assert all(Path(package.__file__).resolve().is_relative_to(snapshot)
|
|
for package in (email_connect, user_engine))
|
|
|
|
messages, provisions, events = [], [], []
|
|
store = InMemoryUserEngineStore()
|
|
store.migrate()
|
|
service = UserEngineService(store=store, identity_adapter=FixtureIdentityClaimsAdapter(),
|
|
authorization=LocalAuthorizationCheckPort())
|
|
ledger = SQLiteDeliveryStore(str(tmp_path / "mail.db"))
|
|
|
|
class MailSink:
|
|
def send(self, recipient, subject, body):
|
|
assert recipient.endswith("@example.test")
|
|
messages.append({"recipient": recipient, "subject": subject, "body": body})
|
|
return "synthetic-provider-reference-" + str(len(messages))
|
|
|
|
def check(self):
|
|
pass
|
|
|
|
class Directory:
|
|
def provision(self, request):
|
|
provisions.append(request)
|
|
return ProvisioningResult("synthetic-directory", request.preferred_username,
|
|
"password_setup_required",
|
|
password_setup_url="https://directory.example.test/setup")
|
|
|
|
oidc = OIDCClient(issuer="https://issuer.example.test", client_id=CLIENT,
|
|
redirect_uri="https://portal.example.test/callback", audience=CLIENT)
|
|
claims = human_actor_claims(tenant=TENANT, subject="synthetic-inviter")
|
|
claims["roles"] = ["tenant-admin"]
|
|
service.me(claims)
|
|
oidc.sessions["synthetic-admin-session"] = BrowserSession(
|
|
claims=claims, expires_at=time.time() + 3600, csrf_token="synthetic-admin-csrf")
|
|
operator = service.identity_adapter.normalize(human_actor_claims(
|
|
tenant="platform:root", subject="synthetic-delivery-operator"))
|
|
portal = PortalApplication(service, trusted_proxy_secret="synthetic-proxy-secret-long-enough",
|
|
login_url="https://issuer.example.test/login", oidc_client=oidc,
|
|
provisioning=Directory(), registration_clients=(CLIENT,),
|
|
registration_tenants=(TENANT,),
|
|
registration_oidc_issuer="https://issuer.example.test",
|
|
registration_password_setup_origins=("https://directory.example.test",))
|
|
|
|
def response(start, payload, content_type="application/json", status="200 OK"):
|
|
data = payload.encode() if isinstance(payload, str) else json.dumps(payload).encode()
|
|
start(status, [("Content-Type", content_type), ("Content-Length", str(len(data)))])
|
|
return [data]
|
|
|
|
def fixtures(env, start):
|
|
path = env["PATH_INFO"]
|
|
if path == "/mailbox":
|
|
body = "<h1>Synthetic mailbox</h1>" + "".join(
|
|
f'<article data-recipient="{html.escape(m["recipient"])}"><pre>'
|
|
+ html.escape(m["body"]) + "</pre></article>" for m in messages)
|
|
return response(start, body, "text/html")
|
|
if path == "/state":
|
|
return response(start, {"messages": len(messages), "provisions": len(provisions),
|
|
"events": len(events),
|
|
"users": service.operability_snapshot().metrics["users"]})
|
|
if env["REQUEST_METHOD"] != "POST":
|
|
return response(start, {}, status="404 Not Found")
|
|
raw = env["wsgi.input"].read(int(env.get("CONTENT_LENGTH") or 0))
|
|
data = json.loads(raw or b"{}")
|
|
if path == "/events":
|
|
assert env.get("HTTP_AUTHORIZATION") == "Bearer synthetic-event-token"
|
|
assert "primary_email" not in data["data"]
|
|
events.append(data)
|
|
elif path == "/deliver":
|
|
service.deliver_outbox(operator, portal.outbox_delivery, worker_id="fixture-worker")
|
|
elif path == "/expire":
|
|
with ledger.lock, ledger.db:
|
|
ledger.db.execute("UPDATE verifications SET expires_at=? WHERE email=?",
|
|
("2000-01-01T00:00:00+00:00", data["email"]))
|
|
elif path == "/suppress":
|
|
ledger.suppress(data["email"])
|
|
else:
|
|
return response(start, {}, status="404 Not Found")
|
|
return response(start, {"ok": True})
|
|
|
|
with server(fixtures, ports) as fixture_url, server(portal, ports) as portal_url:
|
|
policies = [CallerPolicy(
|
|
caller_id=name,
|
|
token=TOKEN if name == "user-engine" else "synthetic-other-caller-token",
|
|
source="user-engine", bindings=frozenset({(TENANT, CLIENT)}),
|
|
operations=frozenset({"send", "status", "delivery-status",
|
|
"verify", "consume", "cancel"}),
|
|
templates=frozenset({"family_member.invited", "family_invitation.resent"}),
|
|
rate_per_minute=120, burst=100, max_payload_bytes=32768,
|
|
) for name in ("user-engine", "other-caller")]
|
|
receiver = ScopedTransactionalApplication(ledger, MailSink(), portal_url, policies)
|
|
with server(receiver, ports) as mail_url:
|
|
portal.registration_verification = HTTPRegistrationVerificationAdapter(
|
|
base_url=mail_url, bearer_token=TOKEN)
|
|
portal.outbox_delivery = HTTPOutboxDeliveryAdapter(
|
|
event_url=fixture_url + "/events", event_bearer_token="synthetic-event-token",
|
|
mail_url=mail_url + "/v1/send", mail_bearer_token=TOKEN,
|
|
mail_client_bindings=json.dumps([[TENANT, CLIENT]]))
|
|
result = subprocess.run(
|
|
[os.environ["OWNER_NODE"], os.environ["OWNER_BROWSER_SCRIPT"],
|
|
portal_url, fixture_url, mail_url, os.environ["OWNER_PLAYWRIGHT_MODULE"]],
|
|
capture_output=True, text=True, timeout=150)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
checks = json.loads(result.stdout)
|
|
assert len(provisions) == 1
|
|
assert provisions[0].preferred_username == "verified.person"
|
|
assert all(p.primary_email.endswith("@example.test") for p in provisions)
|
|
with ledger.lock:
|
|
assert ledger.db.execute("SELECT count(*) FROM verifications WHERE consumed_at IS NOT NULL"
|
|
).fetchone()[0] == 1
|
|
assert ledger.db.execute("SELECT count(*) FROM verifications WHERE canceled_at IS NOT NULL"
|
|
).fetchone()[0] == 1
|
|
ledger.db.close()
|
|
assert checks["passed"] and checks["browser_requests_outside_fixture"] == 0
|
|
Path(os.environ["OWNER_CHECKS_RESULT"]).write_text(json.dumps(checks))
|