From c560407c741cd19201a1e1c0d8e6d87620aadb98 Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Mon, 2 Mar 2026 16:22:51 +0100 Subject: [PATCH] integration zemroo, image --- ca_core/crypto/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 138 bytes .../zenroom_client.cpython-313.pyc | Bin 0 -> 6695 bytes .../zenroom_service_client.cpython-313.pyc | Bin 0 -> 7936 bytes ca_core/crypto/zenroom_client.py | 157 ++++++++++++++++++ ca_core/crypto/zenroom_service_client.py | 148 +++++++++++++++++ ...integration_zenroom_docker.cpython-313.pyc | Bin 0 -> 4299 bytes ...ntegration_zenroom_service.cpython-313.pyc | Bin 0 -> 1109 bytes .../test_zenroom_client.cpython-313.pyc | Bin 0 -> 5945 bytes ...est_zenroom_service_client.cpython-313.pyc | Bin 0 -> 6790 bytes ...nroom_service_client_clean.cpython-313.pyc | Bin 0 -> 2991 bytes ...integration_zenroom_docker.cpython-313.pyc | Bin 0 -> 4311 bytes ...service_client_integration.cpython-313.pyc | Bin 0 -> 1939 bytes .../test_integration_zenroom_docker.py | 88 ++++++++++ tests/integration/test_zenroom_live.py | 78 +++++++++ ...test_zenroom_service_client_integration.py | 30 ++++ tests/test_integration_zenroom_service.py | 15 ++ tests/test_zenroom_client.py | 90 ++++++++++ tests/test_zenroom_service_client.py | 67 ++++++++ tests/test_zenroom_service_client_clean.py | 55 ++++++ 20 files changed, 729 insertions(+) create mode 100644 ca_core/crypto/__init__.py create mode 100644 ca_core/crypto/__pycache__/__init__.cpython-313.pyc create mode 100644 ca_core/crypto/__pycache__/zenroom_client.cpython-313.pyc create mode 100644 ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc create mode 100644 ca_core/crypto/zenroom_client.py create mode 100644 ca_core/crypto/zenroom_service_client.py create mode 100644 tests/__pycache__/test_integration_zenroom_docker.cpython-313.pyc create mode 100644 tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc create mode 100644 tests/__pycache__/test_zenroom_client.cpython-313.pyc create mode 100644 tests/__pycache__/test_zenroom_service_client.cpython-313.pyc create mode 100644 tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc create mode 100644 tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc create mode 100644 tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc create mode 100644 tests/integration/test_integration_zenroom_docker.py create mode 100644 tests/integration/test_zenroom_live.py create mode 100644 tests/integration/test_zenroom_service_client_integration.py create mode 100644 tests/test_integration_zenroom_service.py create mode 100644 tests/test_zenroom_client.py create mode 100644 tests/test_zenroom_service_client.py create mode 100644 tests/test_zenroom_service_client_clean.py diff --git a/ca_core/crypto/__init__.py b/ca_core/crypto/__init__.py new file mode 100644 index 0000000..1a9daba --- /dev/null +++ b/ca_core/crypto/__init__.py @@ -0,0 +1 @@ +# Crypto HTTP client for Zenroom suite. diff --git a/ca_core/crypto/__pycache__/__init__.cpython-313.pyc b/ca_core/crypto/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0df9627e168eff84e99f8e58c9de2c7b122257f GIT binary patch literal 138 zcmey&%ge<81bKl=Gc|zpV-N=h7@>^MEI`IohI9r^M!%H|MNB~6XOPq_P5q4g+*JM2 z;?yGjg6vHF< literal 0 HcmV?d00001 diff --git a/ca_core/crypto/__pycache__/zenroom_client.cpython-313.pyc b/ca_core/crypto/__pycache__/zenroom_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..287855be522dc0921d07dd762b86386b2df258f1 GIT binary patch literal 6695 zcmcIITWlLwc9(B*#Ft2kdfK{@Wm!y2(Xk~bRupesI~!Ye?5xJzbuDe^5jm75B4?O8 zL(39SAYThDu*J&ldhKijL@5l^`C5PGtH^$}UmJ4e1&N8<3h1Ih{+QUUi+uH*c~G*f zq=?(Szi;|eX#{c(*GqkE+!qi`)xs6OnP+J zDS1VApOJE!?!Q#lBt;gAx}O){tB7(=)IAv)x^gB6Gj~{xN8M1lMdk?726Q=?i+3_N z?_!<>H$Vcgd9kQA5Ad$>AkX!Zcirzg7_?fywlNPMgjFF}#ToO%ty-j2i|&HizN;MoEdwZ~o{fk3 zn6Z=z(6HhPRZBv#NCi1hK{lE&UlhmaIgJ*S90*0Fiag0tnxa2pk|xOuu(({& zR645Wm{iu%c|jA>OX7-}&M9&sNi~HE0H{=^%L-e{OK^fnVu6BM0O}MbYXWEs zquQdNS*i!SSp=GkM6JS%Wo3b(wodbxE>fkU0RpDxmK9Z%0AY?hrK*(@OxcEuM6!aA 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 E54dRd*8l(j literal 0 HcmV?d00001 diff --git a/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc b/ca_core/crypto/__pycache__/zenroom_service_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d90133f9687528b82c829e56b8ff69b2ad1ab1a7 GIT binary patch literal 7936 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 -k -c + + If your docker image/entrypoint differs, pass `zenroom_args` accordingly. + + Note: This module is named *zenroom_client.py* (not zenroom.py) to avoid + potential import shadowing with future packages/modules. + """ + + def __init__( + self, + image: str = "zenroom/zenroom:latest", + docker_bin: str = "docker", + work_mount_path: str = "/work", + zenroom_args: Optional[Sequence[str]] = None, + timeout_s: int = 30, + ) -> None: + self.image = image + self.docker_bin = docker_bin + self.work_mount_path = work_mount_path + self.zenroom_args = list(zenroom_args) if zenroom_args is not None else ["zenroom", "-z"] + self.timeout_s = timeout_s + + def run( + self, + script: str, + *, + data: Optional[JsonLike] = None, + keys: Optional[JsonLike] = None, + conf: Optional[JsonLike] = None, + extra_docker_args: Optional[Sequence[str]] = None, + extra_zenroom_args: Optional[Sequence[str]] = None, + ) -> Union[dict, str]: + """Execute a Zenroom script. + + Args: + script: The Zenroom script (text). + data/keys/conf: Optional JSON-like payloads written to files. + extra_docker_args: Optional extra args inserted after `docker run`. + extra_zenroom_args: Optional extra args appended before the script path. + + Returns: + Parsed JSON dict if stdout is valid JSON, otherwise raw stdout string. + + Raises: + ZenroomError on non-zero exit. + """ + if not isinstance(script, str) or not script.strip(): + raise ValueError("script must be a non-empty string") + + with tempfile.TemporaryDirectory(prefix="zenroom_") as tmpdir: + workdir = Path(tmpdir) + + # Defensive: TemporaryDirectory normally creates the dir, but tests may mock it. + workdir.mkdir(parents=True, exist_ok=True) + + script_path = workdir / "script.zen" + script_path.write_text(script, encoding="utf-8") + + data_path = None + keys_path = None + conf_path = None + + if data is not None: + data_path = workdir / "data.json" + data_path.write_text(json.dumps(data), encoding="utf-8") + if keys is not None: + keys_path = workdir / "keys.json" + keys_path.write_text(json.dumps(keys), encoding="utf-8") + if conf is not None: + conf_path = workdir / "conf.json" + conf_path.write_text(json.dumps(conf), encoding="utf-8") + + cmd = [ + self.docker_bin, + "run", + "--rm", + "-i", + ] + + if extra_docker_args: + cmd.extend(list(extra_docker_args)) + + # Mount temp dir into container + cmd.extend( + [ + "-v", + f"{workdir}:{self.work_mount_path}", + "-w", + self.work_mount_path, + self.image, + ] + ) + + # Build zenroom command + cmd.extend(list(self.zenroom_args)) + + if data_path is not None: + cmd.extend(["-a", str(Path(self.work_mount_path) / data_path.name)]) + if keys_path is not None: + cmd.extend(["-k", str(Path(self.work_mount_path) / keys_path.name)]) + if conf_path is not None: + cmd.extend(["-c", str(Path(self.work_mount_path) / conf_path.name)]) + + if extra_zenroom_args: + cmd.extend(list(extra_zenroom_args)) + + cmd.append(str(Path(self.work_mount_path) / script_path.name)) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=self.timeout_s, + check=False, + ) + + if result.returncode != 0: + stderr = (result.stderr or "").strip() + stdout = (result.stdout or "").strip() + msg = stderr or stdout or f"Zenroom failed with exit code {result.returncode}" + raise ZenroomError(msg) + + out = (result.stdout or "").strip() + if not out: + return "" + + try: + parsed = json.loads(out) + if isinstance(parsed, dict): + return parsed + # Zenroom can output arrays too; keep compatibility. + return {"result": parsed} + except json.JSONDecodeError: + return out diff --git a/ca_core/crypto/zenroom_service_client.py b/ca_core/crypto/zenroom_service_client.py new file mode 100644 index 0000000..1ad8473 --- /dev/null +++ b/ca_core/crypto/zenroom_service_client.py @@ -0,0 +1,148 @@ +import json +import urllib.request +import urllib.error +from typing import Any, Dict, Optional + + +class ZenroomServiceError(RuntimeError): + pass + + +class ZenroomServiceClient: + + def __init__( + self, + base_url: str = "http://localhost:3300", + *, + api_prefix: str = "/api", + timeout_s: int = 10, + ) -> None: + self.base_url = base_url.rstrip("/") + self.api_prefix = api_prefix.strip() + + if self.api_prefix in {"", "/"}: + self.api_prefix = "" + elif not self.api_prefix.startswith("/"): + self.api_prefix = "/" + self.api_prefix + + self.timeout_s = timeout_s + + def _make_url(self, path: str) -> str: + path = "/" + path.lstrip("/") + return f"{self.base_url}{self.api_prefix}{path}" + + def _request_json( + self, + method: str, + path: str, + payload: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + + url = self._make_url(path) + data = None + headers = {"Accept": "application/json"} + + if payload is not None: + data = json.dumps(payload).encode("utf-8") + headers["Content-Type"] = "application/json" + + req = urllib.request.Request(url, data=data, headers=headers, method=method.upper()) + + try: + with urllib.request.urlopen(req, timeout=self.timeout_s) as resp: + raw = resp.read() + text = raw.decode("utf-8") + except urllib.error.HTTPError as e: + body = "" + try: + body = e.read().decode("utf-8") + except Exception: + pass + raise ZenroomServiceError(f"HTTP {e.code} from {url}: {body or e.reason}") from e + except urllib.error.URLError as e: + raise ZenroomServiceError(f"Failed to reach {url}: {e.reason}") from e + + text = text.strip() + if not text: + raise ZenroomServiceError(f"Empty response from {url}") + + try: + parsed = json.loads(text) + except json.JSONDecodeError as e: + raise ZenroomServiceError(f"Non-JSON response from {url}: {text[:200]}") from e + + if not isinstance(parsed, dict): + raise ZenroomServiceError(f"Expected JSON object from {url}") + + return parsed + + def _post(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]: + return self._request_json("POST", path, payload) + + def _post_data(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]: + res = self._post(path, {"data": data}) + + if "zenroom_errors" in res or "exception" in res: + exc = res.get("exception", "") + ze = res.get("zenroom_errors") + logs = "" + if isinstance(ze, dict): + logs = str(ze.get("logs", ""))[:800] + raise ZenroomServiceError(f"Zenroom error from {path}: {exc or logs or 'unknown error'}") + + return res + + @staticmethod + def _require_non_empty_str(name: str, value: str) -> str: + if not isinstance(value, str): + raise TypeError(f"{name} must be a string") + v = value.strip() + if not v: + raise ValueError(f"{name} cannot be empty") + return v + + def generate_keypair(self, my_name: str) -> Dict[str, str]: + """Generate an ECDH keypair using the RESTroom contract: + + POST /api/Generate-a-keypair,-reading-identity-from-data + body: { "data": { "myName": "" } } + + Observed response from your service: + { "": { "keyring": { "ecdh": "" } } } + + Some variants also include: + "ecdh_public_key": "" + + This method returns: + { "private_key": "...", "public_key": "..." } (public_key only if present) + """ + my_name = self._require_non_empty_str("my_name", my_name) + + res = self._post_data( + "Generate-a-keypair,-reading-identity-from-data", + {"myName": my_name}, + ) + + if not res: + raise ZenroomServiceError("Empty keypair response") + + # Zenroom typically returns { "": { ... } } + owner = next(iter(res.values())) + if not isinstance(owner, dict): + raise ZenroomServiceError(f"Invalid keypair response structure: {res!r}") + + keyring = owner.get("keyring") + if not isinstance(keyring, dict): + raise ZenroomServiceError(f"Invalid keypair response (missing keyring): {res!r}") + + private_key = keyring.get("ecdh") + if not isinstance(private_key, str) or not private_key.strip(): + raise ZenroomServiceError(f"Invalid keypair response (missing keyring.ecdh): {res!r}") + + out: Dict[str, str] = {"private_key": private_key} + + public_key = owner.get("ecdh_public_key") + if isinstance(public_key, str) and public_key.strip(): + out["public_key"] = public_key + + return out diff --git a/tests/__pycache__/test_integration_zenroom_docker.cpython-313.pyc b/tests/__pycache__/test_integration_zenroom_docker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02703a0b7c28f9fdfd3fcba6d5711fafe57511b4 GIT binary patch literal 4299 zcmb7H+jA4w89%$L)y*pj-5lcvSYH4u_)>!nu_jy;Us3^)sn+5l_H;*<)<#&~RnD#w zTf=mKnHESoW2X!gW|{~0p;Nmr&12p=BaKAPGs#IfN@UgBkkFVwTn% z=AdTJ+0OEq7tsXYiYDy+OvOUdYnj}xC60#&$=20?5Oz*5$-s7>r6t3bwp~kTq5*6> zE!+KUrB5X5;?D_ZJ=k;Bi@oq{Z${WBIVKwVSjpLpF5B>)Ce(@~S2L2_6Kt||+p4AY za}^B-rkO~?WAc!QFsAK7+PL(X?8bCt(sD+_N`y6oA1NxZo$ zW31s!#acze@gib=D+-40p#5mP)|;$uqSgQkER>Ph9=2|jt-ima*Y#TNsv9J6l1<_z z`+!|VDU=lIx~i(=j(&pgFX|L8(%oIAw9Y9k| z#QQvyRE#24#SU$zQ^ZPI*VIV2Y0pwWn2t{rtmia!>@j)A$O>=Tbt9`44O%N0n>It4 zGfan(n^j0NpSh%DW+LoJDO52b4xW#Ts%D5&T2alG!qrwfOq@#R@=7);CNOcDiAuq0 zH)0256Dz30&I0)Slm+97vlLf406kRttTvP!}@e9MIT^bu2O^uoU5ipm~j1lrG z3L4WjJeN@l25iA}W4hCfmQ^tEj>+o=&J`k@$rMaRF*#VjPCzA!qIsg{k~XXK6mt++S|&E&F=Q&fblN;D4U^k+5$I zF?`oweVq>*LPR}E$965Jm!-BSbA{UV^`}<+; zTOUL8Fb~q?bF=AbB?H8(l^PxY0Zng8M~O?J9$Ovh{(nkRLP`#xB$Hy2H5$~`1+j@z z(gD%YE;XdU8&xt1FOZ4QhJ`Usi*QxiurZ(>&tsqQCPSp!a#u&pS zE{tFg$kM(lnCW?m)e$dXz9jA#R3nO**D~pRem*MV5ZH>?F^S)}5rpX&8oroFoj;G8 zNK-QrKLQcq=vweT(y$+d1!gS5gXxj!I%H)o2T;a`psk$@`UMDrO*bE{!{(<;E`XEJ zOM!WCHF)sV2yNVnP{885--N$$lpD{KeP_zfGZc#rNC*=EMkxUMzfOM*030a$2JSlt zC;+3+b+Yj51F;~w7j(zExqJKUu?TmsjUjr32bqBZwL&5WDO-IY?w|=ovM6Lw2C^rM zZ4#MO`tBoH`wKi{eMaiR9?$hw{+q;JM+X98Zi5e~^rg#YYn-d)@9h zArlPG19~8$)K7?i82eyY?8qup>0;i9iq&8faqr+BVGW5|TR#Iv-g$#WXl46aG6GPI z*SMG5CG|o2WUW~WGfuEii*>q1@I@hz+D>n8U@=v1(K3i$k-Q=GT6dEa%9G0uE#rh{ zv_Qh$h5go=Us~VZAKq~a(r*{owrnWOG_WvXHow}PrBg#RYXa448A8nFXd26k9RP{f zVj*IBEZfxcrlacck|=ge7IXQmm@`DcUcDhYG7@o^tfrgxX~j^~PfTY~%^3u%rc;{9 z6$r>_E~>-_!hA@2Tqdu>tQpf@$@)8^g&SUyDh%e+kZBLUjwRjO17l!VzYgLmdgSvj z%zrjt-rK+KJHF~W{xHzI9%%b>psn1VT?;5>L3tEDaC_pG`zHH$pTE|-G_n>rQWlOp z3N?Ni|8@MXwmiQU8o0^*%JnGRx^(RK1HT4l{*{uafDUuIQK`gr@EYi%$~$6bPN5P82K2xrv^;s;MOfYa~S* zGNyCQ{vUSNF4I!v1?2Ewa+$}!?%BWU+5gp*`<~7XA+T`v*4gq~N52z}{hLGHu60l6 zUp$@9b@-A*D}k7eyJvI90*)2cFRo7zAO4Wmz}iN}Z9@23*P~i*O#YjEBhp zvJHAlnD~ea(xe{-@k{iCb@0y30J8g^umb;6W{U%Lld4bO=J?3>ZjSG(*dWs*LEH@$ zP-tGqKL(B7t4v1$K1%bsNlZcn+2<5|Nsz@C(ipx(b<;&Y8)P!dv{7k?`y8+lzegs7 zGkCsWXx-I-EmtW?2~3^eLf&+U+9v)SOv7Bdo1Hr}o$pnS^%TAcg8_<2p8)|kbcT6= zTn~`@0rEXS{=cE7CwAoVFE)LiSQlDXh1R9sRpH>WYgOpI>e@iu4d*rI4c|52BJ;(u w#rfN(zC*45LW583K_;}>xrYhe4L(CqJv-FG^ew%+-qyF;*7rSv2YOWh1EgxVApigX literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc b/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7971df79b1909bde10caecec8bbb3848610f0a6 GIT binary patch literal 1109 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 literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..55af9bbe605f48575b458d7fe29980a1d407948c GIT binary patch literal 6790 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 XUzFqgcWy4V^cGuszbE)*?$`eTTKLo( literal 0 HcmV?d00001 diff --git a/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5897f9ff8c86efb429fa23506cc729905907d4d7 GIT binary patch literal 2991 zcmai0-ER{|5a0W<&-U34C4nT+;+T)tZ4-xvsGs7YEnf*rsV_z?xH?_zO?(ibU0i=A{z6_Yc7$RidSpsH#5j1~sU!ow>_}U_v_&znz`got?RznY~Uj z87DyXAAh#KQ3&}HCm}`FnaJs2%}S^(c$Ysks>6W*jx=>GA35Qw8ca9NVS$ljw*%<5XJZ@B};$ zd5oje1F%Fs>8ZgO&>XoXOx+T=V&JZv$Yqz-W*K~c;^MYUhQFP!I(3?Fx|HP`^H$!Y zuIKt_ECg{Hpu#|nvL7i7)U-z$Ev_1dWm}$M@WE%C9SL=q2W}MLGFj^z_%_}FmXz1C z1bKqI@Bbh$OQc8&DD9$sgkks$&D|0hhU((R4BH0CWk$^k-|?&)D90791^_Okw_jVh zx^P<^V7(yN+7B!Q^keFtY&;7l2$A#KEt z!X*M(C{7(^_#tw#A0$`lrLy6*8k9@GanYM`acu@D2pw*LUV8p51iGED#l`BwPtfc8 z;eX1hTpX{_HtQ5(i5o`UsWfZIYld;YY1Y;?X~UScnCsOno7xVjm6;Y~3d0I!Xq@dr z7y{sZhT(dqXO#`p^O!Z$goTFT2NfKQ_rYQz;I+Ch0(?mxh=MYBqxgux@ONY#dLIMV z7wp7MPgNS`4N~VrfTt_1Bp3W%=j%5BdW0DE5v;=Tbq!RZ%%XEjF2eh#p#-+7@%tF# z!l5#2H9Tiz>*^S3vRciW8DaE%6U^Y$X;AyAg2j9A6aE*terU9U4h=zC|PLT$s&S!@G7=}}#MTd<{~RKVNlB0U!F zwq+>Jbe9^YMOlygtz8UTZwAYF>y-(ArMzjSP{4BTB3<*>`DiQ8v8HVGgy_jlw#ilx zN>M3bMR!qQpi2N#Jr%|tju|pXa_K3qe00&Kj7Lrt-=944DVJ%vQsvQ8g%hV?3Y|8M z0;AEKsX-yk(-w~cFw34>P@3NC@LLOgRnKd@lh4XbKGLTb#`0#v${(ZP6=;XeVbC{Bi@iRKJrE8a zwkpuHEUz^@%bfaf#q`Wv(yv1Cs(zhwl=iZa|9eprXEXjEFQE}d~2q1D%%Sq+lR0p;Q#_Q zDXuh3uUuuvQSb@^?%34GU&Sc`c1L#)z?bBn61#Hdi!---j^0s@JroC&ct;|U<3b17 zU@z{-ZDVi2z||)FM&NY>CrU2d8>E3(g-*(_U*Q`yYx(aI$Qh)u3&{paYTzm^`xvp{ zCy9An0z3T0|LoF!HV3J%Vt#x873as$6&zRJ3m(M7>@>K6ZP}dxcpwRaaF-(PWu1Qdan0>J+-Q3+gf({U|ZXNGuGA)UXHC1>5BTfdL{jN`nvGl*!9-;hgV4U iFLL;wNJ8j)NB|z~-Yvv$Tw2{T+TJtzn83vUNdE$PBdXZ| literal 0 HcmV?d00001 diff --git a/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc b/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b690dd38c47ec1bf914c519d7f85a4e280d1ca10 GIT binary patch literal 4311 zcmb7H+jA4w89%$LyH^srImQjJz5w3fOAR)OHQ}Q8k_w1SwH6Ptr#rH=Hp23*a(0#2 z8m0rxv_R4sJ7t(K(>%Bjnb>`49`n{Ae}PX6Q4ESz2?L zgPJ{SJ7dRo0ZrIj(S)O)saQz*ER)-{#M$9NvUN2egk2L%GPvDmX~~GCZPyZ-XaL(T z%XU9o=@ZDh>}UBiUhF;N!#;R+G$ZVnoD&UwtmJA&muz@X6KX}0yBSHI2{zffZPn8H zxr&B^(~Q{gh&&`j#d2`D)`Z z#v0C4tW_i&FCyl*qG0GY+Kia8tU9aV?x+giWBb^4 zH0^Cc<1TBw6QyDQ0y==Eo#PaCqz78TqRPNbSBk03)Y${zCY6aX?bUjUnL;PH189m7 zeb2&4#VBG`=+I_51+1iXO%=OMN0$1*bbhR0J*TN-kH|YlR_vxjH?msMptXXrX)}~L z!*m+CS%oy)GZ&T2jL43Z!W9$3;JLVy&daAcl$i=HFMt~H<50~ zdlBwCKST709i(Z`&8DZ743Mx^YSjD(G`%i0B`$?}Y;^?u|LIA|DLH|jOo~a?C{bG% z#3@QoCqznz)Q|#iREfnuM=U}i7RIMCs#(;)EC8+Bt6Pg^WMwV~u*Qd=t(^?|6$pY&k3Cw4%}#djeP4YH=Zv0PnTV%DHanSlYdLZSyLTYVsIqX|SZDr8dz zGAWB~6LyJfMpj_O4#@`D%qiJHx+Dj7Pdi1I`Q{iU7j22pelj)|A5EO- z^?2fhOfWnT=z$PZKOz1>?EPV(BdbiMi+Lj|RD+G^*}*-+8WO>_egTZU{TfNp%KWv& z1fUwPaWA<`8iZ8JTC)^poM4|8>vW6Yi$XB9o$6l0VyfPvWe~k2kwY4`?kOphCl?=D z#tF@6f#kaj`>i#VT1*+FlhLFwCl$I4b z01~gnf@pdz+tl->v+D4YAaqO?bNQ^0GX%h1y&*bM6rCok>84{^F%z{@+rREVzUn{zAlSSfZ2NPtt=yhn3o2z^c^ElxYvQKo2K#q^pw_!IvKBm2 z=8rrKH+~WSZTyb5JiitmxWWC#{V>wHbnNSa-w&)sx?Y@B?iyWfA6*NcFZ1U&IkYFZ z1**;U7Y?>Upn@?xqGe-OhV}HZukTAytW%qau+v@*CLh0 zQ5FdZ?g6#<1h1%;ZI{6noc`*a(SkZrEgAwardV>x7T`B+U7EQB9spaVy<6>&~UxOOupi)WICrSIwlCAsas_638J3_p%c^zLM1sj(Q{WdwWMH; zq-aCNbgtR|~@;OyeHRyKAjvDhGjiI13;JXc&O?^* zFd0C$K~D)2A5lS?^ur*2jUKa3yK6Iu9D&CyZ~r;7#eup>)hBOqcJT)fXYZ@nAk)PV z?uH5|G_T{If=2IErn3MasQKI^CLx0Ca|*sH$l?oW3}2r(G3e{k8o%J*JzZy@fv>#FOz|EhnH z`TW@8{H>GUqt<_+!6%Lo6W;9H!-VgIo+7B89%^Cwmfl%!>sxK>`vJiNJ*xi!Qc1i3 literal 0 HcmV?d00001 diff --git a/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc b/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b892a8d01626c1a80e7e36b6818bbde773c91192 GIT binary patch literal 1939 zcmb7E%}*Og6rbJouGeP4geIy*ffy5_8VL-w2u+EQT9itJU@C8X36@5SGdL^kwL7~; zg;R2ALVJL!LgMH{Dqfyii3C90|iZd6Y1eY5sL;!-tzhqrIu*S`1W_omU= z8Ad>*+K=W>5<AdYhKq|l7a zXnv~muP(`O&;z3#2+?3p%yggId@1vuFP(pNlnX=dpr3nEY}#VH9(lgwjz+Z@ilR8u zm0qL=b3&%~JldBgp+`=%s~{xQ6E3C`0tsj&iV2jvi-3QRr>M{)#lthpX-6OVhQEhXULkiSz%(8XDN6)(N-%e*17Z>nnpX#@9Hgk95{d76j zu8i5l$+xb|`&$4wA5`Nj(C(n6Guu*Y=m=%FPaEliceUb8!4k4UCfH_%Bed4aWre3$cldZF$6vUlXl9AdVv3F=?q?PMMT>D5%ME4m;Pz<2kch{fReM~G&mZ!AoAZT zkpFiDUc|dqP(THgP|}nm2!N5i3My@TwzMyTL>mmV%r=pu~>}FCm)teBw2;5$;zskbO5S$(mV&Ir@`J=}Zgiv`9e1PS`_YdM)YPehwCHYXzhmG~>#l2qt~OX3sukUM>V2r8pwdZb--=~Ugn~?OgX@Kw=1UCA?PW*#x@J(%MS?V zUM%2o#k4r2=@lN(pQq~;r#yBs-kK|-as@=Bh3`_^>)+2D bp!gqX=C#rzOg;OsJ~Hi&O#g-8;0@w$2qM8? literal 0 HcmV?d00001 diff --git a/tests/integration/test_integration_zenroom_docker.py b/tests/integration/test_integration_zenroom_docker.py new file mode 100644 index 0000000..d410324 --- /dev/null +++ b/tests/integration/test_integration_zenroom_docker.py @@ -0,0 +1,88 @@ +import os +import sys +import unittest +import subprocess +from pathlib import Path + +# Make ca_core importable as the module root (so `import crypto...` works) +code_path = Path(__file__).parents[1] / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_client import ZenroomDockerClient, ZenroomError + + +def _docker_ok(): + """Return (ok, reason).""" + try: + p = subprocess.run( + ["docker", "version"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + timeout=10, + check=False, + ) + except FileNotFoundError: + return False, "docker CLI not found" + except Exception as e: + return False, f"docker check failed: {e}" + + if p.returncode != 0: + out = (p.stdout or "").strip() + return False, f"docker not usable: {out}" + return True, "" + + +def _image_exists(image: str): + """Return (ok, reason).""" + try: + p = subprocess.run( + ["docker", "image", "inspect", image], + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + text=True, + timeout=10, + check=False, + ) + except Exception as e: + return False, f"docker image inspect failed: {e}" + + if p.returncode != 0: + return False, f"docker image not found locally: {image}" + return True, "" + + +class TestZenroomDockerIntegration(unittest.TestCase): + """Integration tests for ZenroomDockerClient. + + Enable by setting: + ZENROOM_DOCKER_INTEGRATION=1 + + Image selection: + ZENROOM_IMAGE (default: zenroom) + """ + + @classmethod + def setUpClass(cls): + if not os.getenv("ZENROOM_DOCKER_INTEGRATION"): + raise unittest.SkipTest("Docker integration disabled (set ZENROOM_DOCKER_INTEGRATION=1)") + + ok, reason = _docker_ok() + if not ok: + raise unittest.SkipTest(reason) + + cls.image = os.getenv("ZENROOM_IMAGE", "zenroom") + + ok, reason = _image_exists(cls.image) + if not ok: + raise unittest.SkipTest(reason + " (build it or set ZENROOM_IMAGE)") + + def test_basic_execution(self): + client = ZenroomDockerClient(image=self.image) + out = client.run("print('hello')") + self.assertIn("hello", str(out)) + + def test_nonzero_exit_raises(self): + client = ZenroomDockerClient(image=self.image) + with self.assertRaises(ZenroomError): + client.run("THIS IS NOT VALID ZENCODE") diff --git a/tests/integration/test_zenroom_live.py b/tests/integration/test_zenroom_live.py new file mode 100644 index 0000000..b045cca --- /dev/null +++ b/tests/integration/test_zenroom_live.py @@ -0,0 +1,78 @@ +import os +import unittest +import sys +from pathlib import Path + +# Import from ca_core (same pattern as other tests) +code_path = Path(__file__).parent.parent.parent / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_service_client import ZenroomServiceClient + + +def _live_enabled() -> bool: + return os.environ.get("RUN_LIVE_ZENROOM", "").strip().lower() in { + "1", "true", "yes" + } + + +@unittest.skipUnless( + _live_enabled(), + "Set RUN_LIVE_ZENROOM=1 to run live Zenroom service smoke tests", +) +class TestZenroomLiveServices(unittest.TestCase): + + @classmethod + def setUpClass(cls): + base_url = os.environ.get("ZENROOM_BASE_URL", "http://localhost:3300").strip() + api_prefix = os.environ.get("ZENROOM_API_PREFIX", "/api").strip() + timeout_s = int(os.environ.get("ZENROOM_TIMEOUT_S", "20")) + + cls.client = ZenroomServiceClient( + base_url=base_url, + api_prefix=api_prefix, + timeout_s=timeout_s, + ) + + def test_keypair_reading_identity_from_data(self): + """ + Tests: + POST /api/Generate-a-keypair,-reading-identity-from-data + Payload wrapped as {"data": {"myName": "..."}} + """ + res = self.client.generate_a_keypair_reading_identity_from_data( + "LiveUser123456" + ) + + self.assertIn("public_key", res) + self.assertIn("private_key", res) + self.assertIsInstance(res["public_key"], str) + self.assertIsInstance(res["private_key"], str) + self.assertTrue(res["public_key"]) + self.assertTrue(res["private_key"]) + + def test_encrypt_decrypt_password_roundtrip(self): + """ + Tests: + POST /api/Encrypt-a-message-with-the-password + POST /api/Decrypt-the-message-with-the-password + """ + plaintext = "Dear Bob, your name is too short, goodbye - Alice." + + encrypted = self.client.encrypt_a_message_with_the_password( + header="A very important secret", + message=plaintext, + password="myVerySecretPassword", + ) + + for k in ("checksum", "header", "iv", "text"): + self.assertIn(k, encrypted) + self.assertIsInstance(encrypted[k], str) + self.assertTrue(encrypted[k]) + + decrypted = self.client.decrypt_the_message_with_the_password( + secret_message=encrypted, + password="myVerySecretPassword", + ) + + self.assertEqual(decrypted.get("textDecrypted"), plaintext) diff --git a/tests/integration/test_zenroom_service_client_integration.py b/tests/integration/test_zenroom_service_client_integration.py new file mode 100644 index 0000000..87c79b3 --- /dev/null +++ b/tests/integration/test_zenroom_service_client_integration.py @@ -0,0 +1,30 @@ +import os +import unittest +import sys +from pathlib import Path + +code_path = Path(__file__).parents[2] / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_service_client import ZenroomServiceClient + + +class TestZenroomServiceClientIntegration(unittest.TestCase): + + @unittest.skipUnless( + os.getenv("ZENROOM_BASE_URL"), + "No ZENROOM_BASE_URL set", + ) + def test_generate_keypair_real_service(self): + client = ZenroomServiceClient(base_url=os.environ["ZENROOM_BASE_URL"]) + + res = client.generate_keypair("IntegrationUser") + + self.assertIn("private_key", res) + self.assertIsInstance(res["private_key"], str) + self.assertTrue(res["private_key"].strip()) + + # public_key may or may not be returned by this contract variant + if "public_key" in res: + self.assertIsInstance(res["public_key"], str) + self.assertTrue(res["public_key"].strip()) diff --git a/tests/test_integration_zenroom_service.py b/tests/test_integration_zenroom_service.py new file mode 100644 index 0000000..51b8a30 --- /dev/null +++ b/tests/test_integration_zenroom_service.py @@ -0,0 +1,15 @@ +import os +import unittest + +from crypto.zenroom_service_client import ZenroomServiceClient + + +class TestZenroomHTTPIntegration(unittest.TestCase): + + @unittest.skipUnless(os.getenv("ZENROOM_BASE_URL"), "No ZENROOM_BASE_URL set") + def test_sign_and_verify_roundtrip(self): + base_url = os.environ["ZENROOM_BASE_URL"] + client = ZenroomServiceClient(base_url=base_url) + + # You must supply real keys here for full integration + self.assertTrue(True) diff --git a/tests/test_zenroom_client.py b/tests/test_zenroom_client.py new file mode 100644 index 0000000..8168602 --- /dev/null +++ b/tests/test_zenroom_client.py @@ -0,0 +1,90 @@ +import unittest +import sys +from pathlib import Path +from unittest import mock + +# Allow imports from ca_core (same pattern as existing tests) +code_path = Path(__file__).parent.parent / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_client import ZenroomDockerClient, ZenroomError + + +class TestZenroomDockerClient(unittest.TestCase): + def _fake_completed(self, returncode=0, stdout="", stderr=""): + cp = mock.Mock() + cp.returncode = returncode + cp.stdout = stdout + cp.stderr = stderr + return cp + + @mock.patch("crypto.zenroom_client.subprocess.run") + def test_run_builds_expected_docker_command(self, m_run): + m_run.return_value = self._fake_completed(stdout='{"ok": true}') + client = ZenroomDockerClient(image="zenroom/zenroom:latest") + + # Patch temp dir so we can assert paths deterministically + with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: + m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" + m_td.return_value.__exit__.return_value = False + + res = client.run("print('hi')", data={"a": 1}, keys={"k": "v"}, conf={"c": 2}) + + self.assertEqual(res, {"ok": True}) + + args, kwargs = m_run.call_args + cmd = args[0] + self.assertIn("docker", cmd[0]) + self.assertIn("run", cmd) + self.assertIn("zenroom/zenroom:latest", cmd) + + # Mount and workdir + self.assertIn("-v", cmd) + self.assertIn("/tmp/zenroom_test:/work", cmd) + self.assertIn("-w", cmd) + self.assertIn("/work", cmd) + + # Zenroom base args + self.assertIn("zenroom", cmd) + self.assertIn("-z", cmd) + + # Input files flags should be present + self.assertIn("-a", cmd) + self.assertIn("/work/data.json", cmd) + self.assertIn("-k", cmd) + self.assertIn("/work/keys.json", cmd) + self.assertIn("-c", cmd) + self.assertIn("/work/conf.json", cmd) + + # Script at end + self.assertEqual(cmd[-1], "/work/script.zen") + + # subprocess.run called with capture_output/text + self.assertTrue(kwargs.get("capture_output")) + self.assertTrue(kwargs.get("text")) + + @mock.patch("crypto.zenroom_client.subprocess.run") + def test_run_returns_raw_stdout_when_not_json(self, m_run): + m_run.return_value = self._fake_completed(stdout="hello") + client = ZenroomDockerClient() + with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: + m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" + m_td.return_value.__exit__.return_value = False + out = client.run("print('hi')") + self.assertEqual(out, "hello") + + @mock.patch("crypto.zenroom_client.subprocess.run") + def test_run_raises_on_nonzero_exit(self, m_run): + m_run.return_value = self._fake_completed(returncode=1, stderr="boom") + client = ZenroomDockerClient() + with mock.patch("crypto.zenroom_client.tempfile.TemporaryDirectory") as m_td: + m_td.return_value.__enter__.return_value = "/tmp/zenroom_test" + m_td.return_value.__exit__.return_value = False + with self.assertRaises(ZenroomError) as ctx: + client.run("print('hi')") + self.assertIn("boom", str(ctx.exception)) + + def test_run_requires_non_empty_script(self): + client = ZenroomDockerClient() + with self.assertRaises(ValueError): + client.run(" ") diff --git a/tests/test_zenroom_service_client.py b/tests/test_zenroom_service_client.py new file mode 100644 index 0000000..24fccdd --- /dev/null +++ b/tests/test_zenroom_service_client.py @@ -0,0 +1,67 @@ +import json +import unittest +from unittest import mock +import sys +from pathlib import Path + +code_path = Path(__file__).parents[1] / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_service_client import ZenroomServiceClient + + +class _FakeHTTPResponse: + def __init__(self, body: bytes): + self._body = body + + def read(self): + return self._body + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + +class TestZenroomServiceClient(unittest.TestCase): + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_generate_keypair_unpacks_private_key(self, m_urlopen): + payload = { + "IntegrationUser": { + "keyring": {"ecdh": "PRIVKEY"}, + } + } + + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + res = client.generate_keypair("IntegrationUser") + + self.assertEqual(res["private_key"], "PRIVKEY") + self.assertNotIn("public_key", res) + + req = m_urlopen.call_args[0][0] + self.assertEqual(req.method, "POST") + self.assertTrue(req.full_url.endswith("/api/Generate-a-keypair,-reading-identity-from-data")) + + sent = json.loads(req.data.decode("utf-8")) + self.assertEqual(sent, {"data": {"myName": "IntegrationUser"}}) + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_generate_keypair_includes_public_key_if_present(self, m_urlopen): + payload = { + "IntegrationUser": { + "ecdh_public_key": "PUBKEY", + "keyring": {"ecdh": "PRIVKEY"}, + } + } + + m_urlopen.return_value = _FakeHTTPResponse(json.dumps(payload).encode("utf-8")) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + res = client.generate_keypair("IntegrationUser") + + self.assertEqual(res["private_key"], "PRIVKEY") + self.assertEqual(res["public_key"], "PUBKEY") diff --git a/tests/test_zenroom_service_client_clean.py b/tests/test_zenroom_service_client_clean.py new file mode 100644 index 0000000..08cb623 --- /dev/null +++ b/tests/test_zenroom_service_client_clean.py @@ -0,0 +1,55 @@ +import json +import unittest +from unittest import mock +import sys +from pathlib import Path + +code_path = Path(__file__).parents[1] / "ca_core" +sys.path.insert(0, str(code_path)) + +from crypto.zenroom_service_client import ZenroomServiceClient + + +class _FakeHTTPResponse: + def __init__(self, body: bytes): + self._body = body + + def read(self): + return self._body + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc, tb): + return False + + +class TestZenroomServiceClient(unittest.TestCase): + + @mock.patch("crypto.zenroom_service_client.urllib.request.urlopen") + def test_generate_keypair_unpacks_keys(self, m_urlopen): + + payload = { + "Owner": { + "ecdh_public_key": "PUBKEY", + "keyring": {"ecdh": "PRIVKEY"}, + } + } + + m_urlopen.return_value = _FakeHTTPResponse( + json.dumps(payload).encode("utf-8") + ) + + client = ZenroomServiceClient(base_url="http://localhost:3300") + res = client.generate_keypair("User123") + + self.assertEqual(res["public_key"], "PUBKEY") + self.assertEqual(res["private_key"], "PRIVKEY") + + req = m_urlopen.call_args[0][0] + self.assertEqual(req.method, "POST") + self.assertTrue( + req.full_url.endswith( + "/api/Generate-a-keypair,-reading-identity-from-data" + ) + )