diff --git a/, b/, new file mode 100644 index 0000000..c52811a --- /dev/null +++ b/, @@ -0,0 +1,78 @@ +# Importing Alpine with node 18 docker image +FROM node:20-alpine + +# Add dependencies +RUN apk add git python3 make g++ + +# Installing restroom (all packages except sawroom) +RUN npx -y create-restroom@next restroom-mw --all --no-@restroom-mw/sawroom --no-@restroom-mw/planetmint + +WORKDIR /restroom-mw + + +# Force old Express behavior (Express 4) so routes like "/api/*" work +RUN yarn remove express || true \ + && yarn add --exact express@4.21.2 \ + && rm -rf node_modules \ + && yarn install --force + + +# Configure restroom +# Set OPENAPI=false if you want to deactivate Swagger for production +ENV CUSTOM_404_MESSAGE="nothing to see here" +ENV HTTP_PORT=3300 +ENV HTTPS_PORT=3301 +ENV OPENAPI=true +ENV FILES_DIR=./contracts +ENV CHAIN_EXT=chain +ENV YML_EXT=yml + +# Adding the exported files +RUN echo "Adding exported contracts from apiroom" + +RUN echo -e "\nScenario 'ecdh': Create the keypair from a name passed from data/keys\n\n# Here we load the identity of the executor\nGiven my name is in a 'string' named 'myName'\n\n# Here we generate and print the keypair\nWhen I create the ecdh key\nThen print my 'keyring'\n"> ./contracts/Generate-a-keypair,-reading-identity-from-data.zen || true + + + +RUN echo -e ""{\"myName\":\"User123456\"}""> ./contracts/Generate-a-keypair,-reading-identity-from-data.data +RUN echo -e "\nScenario 'ecdh': Encrypt a message with the password \nGiven that I have a 'string' named 'password' \nGiven that I have a 'string' named 'header' \nGiven that I have a 'string' named 'message' \nWhen I encrypt the secret message 'message' with 'password' \nThen print the 'secret message'\n"> ./contracts/Encrypt-a-message-with-the-password.zen || true + +RUN echo -e ""{}""> ./contracts/Encrypt-a-message-with-the-password.keys + + +RUN echo -e ""{\"header\":\"A very important secret\",\"message\":\"Dear Bob, your name is too short, goodbye - Alice.\",\"password\":\"myVerySecretPassword\"}""> ./contracts/Encrypt-a-message-with-the-password.data +RUN echo -e "\nScenario 'ecdh': Decrypt the message with the password \nGiven that I have a valid 'secret message' \nGiven that I have a 'string' named 'password' \nWhen I decrypt the text of 'secret message' with 'password' \nWhen I rename the 'text' to 'textDecrypted' \nThen print the 'textDecrypted' as 'string'\n"> ./contracts/Decrypt-the-message-with-the-password.zen || true + +RUN echo -e ""{}""> ./contracts/Decrypt-the-message-with-the-password.keys + + +RUN echo -e ""{\"secret_message\":{\"checksum\":\"76U+nWVZBwBMbOOktCnZug==\",\"header\":\"QSB2ZXJ5IGltcG9ydGFudCBzZWNyZXQ=\",\"iv\":\"R+B2z2pTLkMVGFCuFHnYL5sAIeuolYmgUOdMm2AOvTI=\",\"text\":\"Df8C8Kkd+ngVAi/tGUe905VPTwId4hv+iL31dgylkDaDumI3BpRO5bN1qKfSsBi2KOA=\"},\"password\":\"myVerySecretPassword\"}""> ./contracts/Decrypt-the-message-with-the-password.data +RUN echo -e "\nScenario 'ecdh': Alice encrypts a message for Bob \n\nGiven that I am known as 'sender' \nGiven that I have my valid 'keyring' \nGiven that I have a valid 'public key' from 'reciever' \nGiven that I have a 'string' named 'message' \nGiven that I have a 'string' named 'header' \n\nWhen I encrypt the secret message of 'message' for 'reciever' \nWhen I rename the 'secret message' to 'secret' \n\nThen print the 'secret' \n\n"> ./contracts/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"reciever\":{\"public_key\":\"BBA0kD35T9lUHR/WhDwBmgg/vMzlu1Vb0qtBjBZ8rbhdtW3AcX6z64a59RqF6FCV5q3lpiFNTmOgA264x1cZHE0=\"},\"message\":\"Dear Bob and Carl, if you are reading this, then we are not friend anymore. Goodbye.\",\"header\":\"Secret message for Bob and Carl\",\"sender\":{\"keyring\":{\"ecdh\":\"IStvfSREogWWYLB+DtpaSFqGJYMZMKvLIdGNN/H5DH4=\"}}}""> ./contracts/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography.data +RUN echo -e "\nScenario 'ecdh': Bob decrypts the message from Alice \nGiven that I am known as 'reciever' \nGiven I have my 'keyring' \nGiven I have a 'public key' from 'sender' \nGiven I have a 'secret message' named 'secret' \nWhen I decrypt the text of 'secret' from 'sender' \nThen print the 'text' as 'string' \nThen print the 'header' from 'secret' as 'string'\n"> ./contracts/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"sender\":{\"public_key\":\"BNRzlJ4csYlWgycGGiK/wgoEw3OizCdx9MWg06rxUBTP5rP9qPASOW5KY8YgmNjW5k7lLpboboHrsApWsvgkMN4=\"},\"secret\":{\"checksum\":\"sxoO1vewQmL8skCmfeiFgw==\",\"header\":\"U2VjcmV0IG1lc3NhZ2UgZm9yIEJvYiBhbmQgQ2FybA==\",\"iv\":\"AngaB+wTbAKWFDayWE2yWVSDD1f/w+lI+LkV0B8tIyM=\",\"text\":\"S2+pJNXhLgT46/ztk/XAJOWdl3jWR4svI170Me38bWHmvS3+kqZxkW2GIZJiw4C4GkdJ8MM2lvQJcP/GWM/7k+mc/XQoxI86Yu4RgCPqYJ+sKD0=\"},\"reciever\":{\"keyring\":{\"ecdh\":\"psBF05iHz/X8WBpwitJoSsZ7BiKawrdaVfQN3AtTa6I=\"}}}""> ./contracts/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography.data +RUN echo -e "\nrule check version 3.0.0 \nScenario 'ecdh': Bob verifies the signature from Alice \n\n\n# Here we load the pubkey we'll verify the signature against\nGiven I have a 'public key' from 'signer' \n\n# Here we load the objects to be verified\nGiven I have a 'string' named 'myMessage' \n\n# Here we load the objects's signatures\nGiven I have a 'signature' named 'myMessage.signature' \n\n# Here we perform the verifications\nWhen I verify the 'myMessage' has a ecdh signature in 'myMessage.signature' by 'signer' \n\n# Here we print out the result: if the verifications succeeded, a string will be printed out\n# if the verifications failed, Zenroom will throw an error\nThen print the string 'Zenroom certifies that signature is correct!' \nThen print the 'myMessage' \n"> ./contracts/Verify-asymmetric-cryptography-signature.zen || true + + + +RUN echo -e ""{\"myMessage\":\"Dear Bob, your name is too short, goodbye - Alice.\",\"myMessage.signature\":{\"r\":\"vWerszPubruWexUib69c7IU8Dxy1iisUmMGC7h7arDw=\",\"s\":\"nSjxT+JAP56HMRJjrLwwB6kP+mluYySeZcG8JPBGcpY=\"},\"signer\":{\"public_key\":\"BBCQg21VcjsmfTmNsg+I+8m1Cm0neaYONTqRnXUjsJLPa8075IYH+a9w2wRO7rFM1cKmv19Igd7ntDZcUvLq3xI=\"}}""> ./contracts/Verify-asymmetric-cryptography-signature.data +RUN echo -e "\nScenario 'ecdh': create the signature of an object \nGiven I am 'signer' \nGiven I have my 'keyring' \nGiven that I have a 'string' named 'myMessage' inside 'mySecretStuff' \n\n# Here we are creating 3 signatures and renaming them afterwards, once with a string,\n# once with an array and once with a complex object such as the keypair\n# a signature is a schema containing two base64 key-values: 'r' and 's', read more about ECDSA at \n# https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm\n\nWhen I create the ecdh signature of 'myMessage' \nWhen I rename the 'ecdh signature' to 'myMessage.signature' \n\n# Here we are printing out the signatures \n\nThen print the 'myMessage' \nThen print the 'myMessage.signature' \n\n"> ./contracts/Sign-objects-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"mySecretStuff\":{\"myMessage\":\"Dear Bob, your name is too short, goodbye - Alice.\"},\"signer\":{\"keyring\":{\"ecdh\":\"mukeqwntoJPtAN94jgahUA/ID7NptMLNL84sMPJ++eY=\"}}}""> ./contracts/Sign-objects-using-asymmetric-cryptography.data +RUN echo -e "\n\n# Loading scenarios\nScenario 'ecdh': Create the public key\n\n# Loading the private keys\nGiven I have the 'keyring'\n\n# Generating the public keys\nWhen I create the ecdh public key\n\n\n# Here we pring all the output\nThen print the 'ecdh public key'\n\n"> ./contracts/Generate-public-key.zen || true + + + +RUN echo -e ""{\"keyring\":{\"ecdh\":\"tWJ3bc7SgFQmWghl2lLmitzSCtfFYws1P2x8UW0edhE=\"}}""> ./contracts/Generate-public-key.data + + +# yarn install and run +CMD yarn start diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c52811a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,78 @@ +# Importing Alpine with node 18 docker image +FROM node:20-alpine + +# Add dependencies +RUN apk add git python3 make g++ + +# Installing restroom (all packages except sawroom) +RUN npx -y create-restroom@next restroom-mw --all --no-@restroom-mw/sawroom --no-@restroom-mw/planetmint + +WORKDIR /restroom-mw + + +# Force old Express behavior (Express 4) so routes like "/api/*" work +RUN yarn remove express || true \ + && yarn add --exact express@4.21.2 \ + && rm -rf node_modules \ + && yarn install --force + + +# Configure restroom +# Set OPENAPI=false if you want to deactivate Swagger for production +ENV CUSTOM_404_MESSAGE="nothing to see here" +ENV HTTP_PORT=3300 +ENV HTTPS_PORT=3301 +ENV OPENAPI=true +ENV FILES_DIR=./contracts +ENV CHAIN_EXT=chain +ENV YML_EXT=yml + +# Adding the exported files +RUN echo "Adding exported contracts from apiroom" + +RUN echo -e "\nScenario 'ecdh': Create the keypair from a name passed from data/keys\n\n# Here we load the identity of the executor\nGiven my name is in a 'string' named 'myName'\n\n# Here we generate and print the keypair\nWhen I create the ecdh key\nThen print my 'keyring'\n"> ./contracts/Generate-a-keypair,-reading-identity-from-data.zen || true + + + +RUN echo -e ""{\"myName\":\"User123456\"}""> ./contracts/Generate-a-keypair,-reading-identity-from-data.data +RUN echo -e "\nScenario 'ecdh': Encrypt a message with the password \nGiven that I have a 'string' named 'password' \nGiven that I have a 'string' named 'header' \nGiven that I have a 'string' named 'message' \nWhen I encrypt the secret message 'message' with 'password' \nThen print the 'secret message'\n"> ./contracts/Encrypt-a-message-with-the-password.zen || true + +RUN echo -e ""{}""> ./contracts/Encrypt-a-message-with-the-password.keys + + +RUN echo -e ""{\"header\":\"A very important secret\",\"message\":\"Dear Bob, your name is too short, goodbye - Alice.\",\"password\":\"myVerySecretPassword\"}""> ./contracts/Encrypt-a-message-with-the-password.data +RUN echo -e "\nScenario 'ecdh': Decrypt the message with the password \nGiven that I have a valid 'secret message' \nGiven that I have a 'string' named 'password' \nWhen I decrypt the text of 'secret message' with 'password' \nWhen I rename the 'text' to 'textDecrypted' \nThen print the 'textDecrypted' as 'string'\n"> ./contracts/Decrypt-the-message-with-the-password.zen || true + +RUN echo -e ""{}""> ./contracts/Decrypt-the-message-with-the-password.keys + + +RUN echo -e ""{\"secret_message\":{\"checksum\":\"76U+nWVZBwBMbOOktCnZug==\",\"header\":\"QSB2ZXJ5IGltcG9ydGFudCBzZWNyZXQ=\",\"iv\":\"R+B2z2pTLkMVGFCuFHnYL5sAIeuolYmgUOdMm2AOvTI=\",\"text\":\"Df8C8Kkd+ngVAi/tGUe905VPTwId4hv+iL31dgylkDaDumI3BpRO5bN1qKfSsBi2KOA=\"},\"password\":\"myVerySecretPassword\"}""> ./contracts/Decrypt-the-message-with-the-password.data +RUN echo -e "\nScenario 'ecdh': Alice encrypts a message for Bob \n\nGiven that I am known as 'sender' \nGiven that I have my valid 'keyring' \nGiven that I have a valid 'public key' from 'reciever' \nGiven that I have a 'string' named 'message' \nGiven that I have a 'string' named 'header' \n\nWhen I encrypt the secret message of 'message' for 'reciever' \nWhen I rename the 'secret message' to 'secret' \n\nThen print the 'secret' \n\n"> ./contracts/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"reciever\":{\"public_key\":\"BBA0kD35T9lUHR/WhDwBmgg/vMzlu1Vb0qtBjBZ8rbhdtW3AcX6z64a59RqF6FCV5q3lpiFNTmOgA264x1cZHE0=\"},\"message\":\"Dear Bob and Carl, if you are reading this, then we are not friend anymore. Goodbye.\",\"header\":\"Secret message for Bob and Carl\",\"sender\":{\"keyring\":{\"ecdh\":\"IStvfSREogWWYLB+DtpaSFqGJYMZMKvLIdGNN/H5DH4=\"}}}""> ./contracts/Encrypt-a-message-for-two-recipients-using-asymmetric-cryptography.data +RUN echo -e "\nScenario 'ecdh': Bob decrypts the message from Alice \nGiven that I am known as 'reciever' \nGiven I have my 'keyring' \nGiven I have a 'public key' from 'sender' \nGiven I have a 'secret message' named 'secret' \nWhen I decrypt the text of 'secret' from 'sender' \nThen print the 'text' as 'string' \nThen print the 'header' from 'secret' as 'string'\n"> ./contracts/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"sender\":{\"public_key\":\"BNRzlJ4csYlWgycGGiK/wgoEw3OizCdx9MWg06rxUBTP5rP9qPASOW5KY8YgmNjW5k7lLpboboHrsApWsvgkMN4=\"},\"secret\":{\"checksum\":\"sxoO1vewQmL8skCmfeiFgw==\",\"header\":\"U2VjcmV0IG1lc3NhZ2UgZm9yIEJvYiBhbmQgQ2FybA==\",\"iv\":\"AngaB+wTbAKWFDayWE2yWVSDD1f/w+lI+LkV0B8tIyM=\",\"text\":\"S2+pJNXhLgT46/ztk/XAJOWdl3jWR4svI170Me38bWHmvS3+kqZxkW2GIZJiw4C4GkdJ8MM2lvQJcP/GWM/7k+mc/XQoxI86Yu4RgCPqYJ+sKD0=\"},\"reciever\":{\"keyring\":{\"ecdh\":\"psBF05iHz/X8WBpwitJoSsZ7BiKawrdaVfQN3AtTa6I=\"}}}""> ./contracts/Decrypt-a-message-for-two-recipients-using-asymmetric-cryptography.data +RUN echo -e "\nrule check version 3.0.0 \nScenario 'ecdh': Bob verifies the signature from Alice \n\n\n# Here we load the pubkey we'll verify the signature against\nGiven I have a 'public key' from 'signer' \n\n# Here we load the objects to be verified\nGiven I have a 'string' named 'myMessage' \n\n# Here we load the objects's signatures\nGiven I have a 'signature' named 'myMessage.signature' \n\n# Here we perform the verifications\nWhen I verify the 'myMessage' has a ecdh signature in 'myMessage.signature' by 'signer' \n\n# Here we print out the result: if the verifications succeeded, a string will be printed out\n# if the verifications failed, Zenroom will throw an error\nThen print the string 'Zenroom certifies that signature is correct!' \nThen print the 'myMessage' \n"> ./contracts/Verify-asymmetric-cryptography-signature.zen || true + + + +RUN echo -e ""{\"myMessage\":\"Dear Bob, your name is too short, goodbye - Alice.\",\"myMessage.signature\":{\"r\":\"vWerszPubruWexUib69c7IU8Dxy1iisUmMGC7h7arDw=\",\"s\":\"nSjxT+JAP56HMRJjrLwwB6kP+mluYySeZcG8JPBGcpY=\"},\"signer\":{\"public_key\":\"BBCQg21VcjsmfTmNsg+I+8m1Cm0neaYONTqRnXUjsJLPa8075IYH+a9w2wRO7rFM1cKmv19Igd7ntDZcUvLq3xI=\"}}""> ./contracts/Verify-asymmetric-cryptography-signature.data +RUN echo -e "\nScenario 'ecdh': create the signature of an object \nGiven I am 'signer' \nGiven I have my 'keyring' \nGiven that I have a 'string' named 'myMessage' inside 'mySecretStuff' \n\n# Here we are creating 3 signatures and renaming them afterwards, once with a string,\n# once with an array and once with a complex object such as the keypair\n# a signature is a schema containing two base64 key-values: 'r' and 's', read more about ECDSA at \n# https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm\n\nWhen I create the ecdh signature of 'myMessage' \nWhen I rename the 'ecdh signature' to 'myMessage.signature' \n\n# Here we are printing out the signatures \n\nThen print the 'myMessage' \nThen print the 'myMessage.signature' \n\n"> ./contracts/Sign-objects-using-asymmetric-cryptography.zen || true + + + +RUN echo -e ""{\"mySecretStuff\":{\"myMessage\":\"Dear Bob, your name is too short, goodbye - Alice.\"},\"signer\":{\"keyring\":{\"ecdh\":\"mukeqwntoJPtAN94jgahUA/ID7NptMLNL84sMPJ++eY=\"}}}""> ./contracts/Sign-objects-using-asymmetric-cryptography.data +RUN echo -e "\n\n# Loading scenarios\nScenario 'ecdh': Create the public key\n\n# Loading the private keys\nGiven I have the 'keyring'\n\n# Generating the public keys\nWhen I create the ecdh public key\n\n\n# Here we pring all the output\nThen print the 'ecdh public key'\n\n"> ./contracts/Generate-public-key.zen || true + + + +RUN echo -e ""{\"keyring\":{\"ecdh\":\"tWJ3bc7SgFQmWghl2lLmitzSCtfFYws1P2x8UW0edhE=\"}}""> ./contracts/Generate-public-key.data + + +# yarn install and run +CMD yarn start diff --git a/ca_api/__init__.py b/ca_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ca_api/__pycache__/__init__.cpython-313.pyc b/ca_api/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..2c0c45f Binary files /dev/null and b/ca_api/__pycache__/__init__.cpython-313.pyc differ diff --git a/ca_api/__pycache__/app.cpython-313.pyc b/ca_api/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000..173a217 Binary files /dev/null and b/ca_api/__pycache__/app.cpython-313.pyc differ diff --git a/ca_api/__pycache__/db.cpython-313.pyc b/ca_api/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000..02539ef Binary files /dev/null and b/ca_api/__pycache__/db.cpython-313.pyc differ diff --git a/ca_api/app.py b/ca_api/app.py new file mode 100644 index 0000000..defb0cb --- /dev/null +++ b/ca_api/app.py @@ -0,0 +1,213 @@ +# ca_api/app.py +from __future__ import annotations + +from typing import Any, Dict, Optional + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel, Field + +from ca_api.db import db_cursor + +from ca_core import entity as entity_core +from ca_core import group_member as group_core +from ca_core import property as property_core +from ca_core import metadata as metadata_core + +app = FastAPI(title="PKI CA API") + + +def bad_request(e: Exception) -> HTTPException: + return HTTPException(status_code=400, detail=str(e)) + + +class CreatorIn(BaseModel): + name: str = Field(min_length=1, max_length=100) + public_key: str = Field(min_length=1, max_length=300) + + +class EnrollPersonIn(BaseModel): + name: str = Field(min_length=1, max_length=100) + public_key: str = Field(min_length=1, max_length=300) + creator_id: int + + +class CreateGroupIn(BaseModel): + name: str = Field(min_length=1, max_length=100) + public_key: str = Field(min_length=1, max_length=300) + creator_id: int + ca_reference: str = Field(min_length=1, max_length=100) + + +class AddMemberIn(BaseModel): + group_id: int + member_id: int + role: str = Field(min_length=1, max_length=10) + + +class SetPropertyIn(BaseModel): + entity_id: int + property_name: str = Field(min_length=1, max_length=100) + validation_policy: Optional[str] = Field(default="default", max_length=19) + source: Optional[str] = Field(default=None, max_length=150) + + +class DeletePropertyIn(BaseModel): + entity_id: int + property_name: str = Field(min_length=1, max_length=100) + + +class SetMetadataNameIn(BaseModel): + name: str = Field(min_length=1, max_length=100) + + +class SetMetadataDefensePIn(BaseModel): + defense_p: bool + + +@app.get("/health") +def health() -> Dict[str, Any]: + return {"ok": True} + + +# ---- Entities ---- + +@app.post("/creators", status_code=201) +def create_creator(payload: CreatorIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + creator_id = entity_core.insert_creator(cur, payload.name, payload.public_key) + return {"id": creator_id} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.post("/persons", status_code=201) +def enroll_person(payload: EnrollPersonIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + person_id = entity_core.enroll_person(cur, payload.name, payload.public_key, payload.creator_id) + return {"id": person_id} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.post("/groups", status_code=201) +def create_group(payload: CreateGroupIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + group_id = entity_core.create_group( + cur, + payload.name, + payload.public_key, + payload.creator_id, + payload.ca_reference, + ) + return {"id": group_id} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.get("/entities/{entity_id}") +def get_entity(entity_id: int) -> Dict[str, Any]: + with db_cursor() as cur: + row = entity_core.get_entity(cur, entity_id) + if row is None: + raise HTTPException(status_code=404, detail="Entity not found") + return dict(row) + + +@app.post("/entities/{entity_id}/status") +def set_entity_status(entity_id: int, status: str, changed_by: int) -> Dict[str, Any]: + try: + with db_cursor() as cur: + entity_core.set_entity_status(cur, entity_id, status, changed_by) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +# ---- Group members ---- + +@app.post("/groups/members", status_code=201) +def add_member(payload: AddMemberIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + group_core.add_group_member(cur, payload.group_id, payload.member_id, payload.role) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.get("/groups/{group_id}/members") +def list_members(group_id: int): + with db_cursor() as cur: + rows = group_core.get_members_of_group(cur, group_id) + return [dict(r) for r in rows] + + +# ---- Properties ---- + +@app.post("/properties", status_code=201) +def set_property(payload: SetPropertyIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + property_core.set_property( + cur, + payload.entity_id, + payload.property_name, + validation_policy=payload.validation_policy or "default", + source=payload.source, + ) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.get("/entities/{entity_id}/properties") +def get_properties(entity_id: int) -> Dict[str, Any]: + with db_cursor() as cur: + props = property_core.get_properties(cur, entity_id) + return {"entity_id": entity_id, "properties": props} + + +@app.delete("/properties") +def delete_property(payload: DeletePropertyIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + property_core.delete_property(cur, payload.entity_id, payload.property_name) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +# ---- Metadata ---- + +@app.get("/metadata") +def get_metadata() -> Dict[str, Any]: + with db_cursor() as cur: + return { + "name": metadata_core.get_name(cur), + "comment": metadata_core.get_comment(cur), + "public_key": metadata_core.get_public_key(cur), + "defense_p": metadata_core.get_defense_p(cur), + } + + +@app.post("/metadata/name") +def set_metadata_name(payload: SetMetadataNameIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + metadata_core.set_name(cur, payload.name) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) + + +@app.post("/metadata/defense_p") +def set_metadata_defense_p(payload: SetMetadataDefensePIn) -> Dict[str, Any]: + try: + with db_cursor() as cur: + metadata_core.set_defense_p(cur, payload.defense_p) + return {"ok": True} + except (ValueError, TypeError) as e: + raise bad_request(e) diff --git a/ca_api/db.py b/ca_api/db.py new file mode 100644 index 0000000..ae6560a --- /dev/null +++ b/ca_api/db.py @@ -0,0 +1,24 @@ +# ca_api/db.py +from __future__ import annotations + +import os +from contextlib import contextmanager + +import psycopg +from psycopg.rows import dict_row + +DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/ca") + +@contextmanager +def db_cursor(): + """ + Transaction-per-request. + Uses dict_row because ca_core expects dict-like rows (row["status"], etc.). + """ + conn = psycopg.connect(DATABASE_URL, row_factory=dict_row) + try: + with conn: # commits on success, rolls back on exception + with conn.cursor() as cur: + yield cur + finally: + conn.close() diff --git a/ca_core/__pycache__/entity.cpython-313.pyc b/ca_core/__pycache__/entity.cpython-313.pyc index 782ccd8..0f800de 100644 Binary files a/ca_core/__pycache__/entity.cpython-313.pyc and b/ca_core/__pycache__/entity.cpython-313.pyc differ diff --git a/ca_core/__pycache__/group_member.cpython-313.pyc b/ca_core/__pycache__/group_member.cpython-313.pyc index 3ecf616..8623ce3 100644 Binary files a/ca_core/__pycache__/group_member.cpython-313.pyc and b/ca_core/__pycache__/group_member.cpython-313.pyc differ diff --git a/ca_core/__pycache__/metadata.cpython-313.pyc b/ca_core/__pycache__/metadata.cpython-313.pyc index 6f362b6..cf30916 100644 Binary files a/ca_core/__pycache__/metadata.cpython-313.pyc and b/ca_core/__pycache__/metadata.cpython-313.pyc differ diff --git a/ca_core/__pycache__/property.cpython-313.pyc b/ca_core/__pycache__/property.cpython-313.pyc index 5bf21dd..d0ae1b8 100644 Binary files a/ca_core/__pycache__/property.cpython-313.pyc and b/ca_core/__pycache__/property.cpython-313.pyc differ diff --git a/ca_core/entity.py b/ca_core/entity.py index b1e0b07..26269c0 100644 --- a/ca_core/entity.py +++ b/ca_core/entity.py @@ -1,4 +1,4 @@ -from db_logging import log_change +from .db_logging import log_change def ensure_entity_active(cursor, entity_id): diff --git a/ca_core/group_member.py b/ca_core/group_member.py index d7a108c..3c2d2ff 100644 --- a/ca_core/group_member.py +++ b/ca_core/group_member.py @@ -1,5 +1,5 @@ -from db_logging import log_change -from entity import ensure_entity_active +from .db_logging import log_change +from .entity import ensure_entity_active def _get_entity_type(cursor, entity_id): diff --git a/ca_core/metadata.py b/ca_core/metadata.py index c1c50c6..23b1c11 100644 --- a/ca_core/metadata.py +++ b/ca_core/metadata.py @@ -1,4 +1,4 @@ -from db_logging import log_change +from .db_logging import log_change def _ensure_singleton_row(cursor): diff --git a/ca_core/property.py b/ca_core/property.py index 10d65b7..53fdb61 100644 --- a/ca_core/property.py +++ b/ca_core/property.py @@ -1,5 +1,5 @@ -from db_logging import log_change -from entity import ensure_entity_active +from .db_logging import log_change +from .entity import ensure_entity_active def _validate_validation_policy(validation_policy: str) -> str: diff --git a/tests/__pycache__/test_api_integration.cpython-313.pyc b/tests/__pycache__/test_api_integration.cpython-313.pyc new file mode 100644 index 0000000..b2a50f6 Binary files /dev/null and b/tests/__pycache__/test_api_integration.cpython-313.pyc differ diff --git a/tests/__pycache__/test_api_smoke.cpython-313.pyc b/tests/__pycache__/test_api_smoke.cpython-313.pyc new file mode 100644 index 0000000..84d7f83 Binary files /dev/null and b/tests/__pycache__/test_api_smoke.cpython-313.pyc differ diff --git a/tests/__pycache__/test_api_unit.cpython-313.pyc b/tests/__pycache__/test_api_unit.cpython-313.pyc new file mode 100644 index 0000000..4a0d7f3 Binary files /dev/null and b/tests/__pycache__/test_api_unit.cpython-313.pyc differ diff --git a/tests/__pycache__/test_entity.cpython-313.pyc b/tests/__pycache__/test_entity.cpython-313.pyc index c72f341..7a2b65d 100644 Binary files a/tests/__pycache__/test_entity.cpython-313.pyc and b/tests/__pycache__/test_entity.cpython-313.pyc differ diff --git a/tests/__pycache__/test_group.cpython-313.pyc b/tests/__pycache__/test_group.cpython-313.pyc index 6861895..506f4cb 100644 Binary files a/tests/__pycache__/test_group.cpython-313.pyc and b/tests/__pycache__/test_group.cpython-313.pyc differ diff --git a/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc b/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc index e7971df..7509f7b 100644 Binary files a/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc and b/tests/__pycache__/test_integration_zenroom_service.cpython-313.pyc differ diff --git a/tests/__pycache__/test_metadata.cpython-313.pyc b/tests/__pycache__/test_metadata.cpython-313.pyc index 5e7fde9..5502025 100644 Binary files a/tests/__pycache__/test_metadata.cpython-313.pyc and b/tests/__pycache__/test_metadata.cpython-313.pyc differ diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc index 73d4330..77e8f31 100644 Binary files a/tests/__pycache__/test_property.cpython-313.pyc and b/tests/__pycache__/test_property.cpython-313.pyc differ diff --git a/tests/__pycache__/test_zenroom_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_client.cpython-313.pyc index 3b57272..b191fe3 100644 Binary files a/tests/__pycache__/test_zenroom_client.cpython-313.pyc and b/tests/__pycache__/test_zenroom_client.cpython-313.pyc differ diff --git a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc index 6481604..0fd43fb 100644 Binary files a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc and b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc differ diff --git a/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc index 5897f9f..3abe7f8 100644 Binary files a/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc and b/tests/__pycache__/test_zenroom_service_client_clean.cpython-313.pyc differ diff --git a/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc b/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc index b690dd3..6333830 100644 Binary files a/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc and b/tests/integration/__pycache__/test_integration_zenroom_docker.cpython-313.pyc differ diff --git a/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc b/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc index 25969f1..cc87de1 100644 Binary files a/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc and b/tests/integration/__pycache__/test_zenroom_live.cpython-313.pyc differ 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 index 3b8520b..0d0714b 100644 Binary files a/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc and b/tests/integration/__pycache__/test_zenroom_service_client_integration.cpython-313.pyc differ diff --git a/tests/integration/test_integration_zenroom_docker.py b/tests/integration/test_integration_zenroom_docker.py index d410324..f453d2c 100644 --- a/tests/integration/test_integration_zenroom_docker.py +++ b/tests/integration/test_integration_zenroom_docker.py @@ -8,7 +8,7 @@ from pathlib import Path code_path = Path(__file__).parents[1] / "ca_core" sys.path.insert(0, str(code_path)) -from crypto.zenroom_client import ZenroomDockerClient, ZenroomError +from ca_core.crypto.zenroom_client import ZenroomDockerClient, ZenroomError def _docker_ok(): diff --git a/tests/integration/test_zenroom_live.py b/tests/integration/test_zenroom_live.py index b07762e..114528e 100644 --- a/tests/integration/test_zenroom_live.py +++ b/tests/integration/test_zenroom_live.py @@ -7,7 +7,7 @@ from pathlib import Path code_path = Path(__file__).parent.parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -from crypto.zenroom_service_client import ZenroomServiceClient +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient def _live_enabled() -> bool: diff --git a/tests/integration/test_zenroom_service_client_integration.py b/tests/integration/test_zenroom_service_client_integration.py index cf75970..9380f84 100644 --- a/tests/integration/test_zenroom_service_client_integration.py +++ b/tests/integration/test_zenroom_service_client_integration.py @@ -7,7 +7,7 @@ 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 +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient class TestZenroomServiceClientIntegration(unittest.TestCase): diff --git a/tests/test_api_integration.py b/tests/test_api_integration.py new file mode 100644 index 0000000..04c8f8f --- /dev/null +++ b/tests/test_api_integration.py @@ -0,0 +1,187 @@ +import os +import unittest +from pathlib import Path + +import psycopg +from fastapi.testclient import TestClient + + +# Run these tests only when a real DB is provided. +DBURL = os.getenv("DATABASE_URL") + +ROOT = Path(__file__).resolve().parents[1] +CREATE_TABLES = ROOT / "create_tables.sql" + + +@unittest.skipUnless(DBURL, "DATABASE_URL not set; skipping API integration tests") +class TestApiIntegration(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Ensure the API uses the same DB as the tests. + os.environ["DATABASE_URL"] = DBURL + + # Import after setting env so ca_api.db picks it up. + from ca_api.app import app # noqa: WPS433 + + cls.client = TestClient(app) + + # Recreate tables from scratch for a clean slate. + schema_sql = CREATE_TABLES.read_text(encoding="utf-8") + with psycopg.connect(DBURL) as conn: + with conn.cursor() as cur: + cur.execute(schema_sql) + + def _log_count(self) -> int: + with psycopg.connect(DBURL) as conn: + with conn.cursor() as cur: + cur.execute("SELECT COUNT(*) FROM log") + return int(cur.fetchone()[0]) + + def test_01_health(self): + r = self.client.get("/health") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) + + def test_02_creator_person_roundtrip_and_log(self): + before = self._log_count() + + r = self.client.post("/creators", json={"name": "Alice", "public_key": "pk-alice"}) + self.assertEqual(r.status_code, 201) + creator_id = r.json()["id"] + + # Creating a creator should log at least one entry (your core logs mutations). + after = self._log_count() + self.assertGreaterEqual(after, before + 1) + + r = self.client.post( + "/persons", + json={"name": "Bob", "public_key": "pk-bob", "creator_id": creator_id}, + ) + self.assertEqual(r.status_code, 201) + person_id = r.json()["id"] + + r = self.client.get(f"/entities/{person_id}") + self.assertEqual(r.status_code, 200) + body = r.json() + self.assertEqual(body["id"], person_id) + self.assertEqual(body["name"], "Bob") + self.assertEqual(body["type"], "person") + + def test_03_group_create_add_member_list_members(self): + r = self.client.post("/creators", json={"name": "Admin", "public_key": "pk-admin"}) + self.assertEqual(r.status_code, 201) + creator_id = r.json()["id"] + + r = self.client.post( + "/groups", + json={ + "name": "Engineering", + "public_key": "pk-eng", + "creator_id": creator_id, + "ca_reference": "ca-ref-001", + }, + ) + self.assertEqual(r.status_code, 201) + group_id = r.json()["id"] + + r = self.client.post( + "/persons", + json={"name": "Carol", "public_key": "pk-carol", "creator_id": creator_id}, + ) + self.assertEqual(r.status_code, 201) + member_id = r.json()["id"] + + r = self.client.post( + "/groups/members", + json={"group_id": group_id, "member_id": member_id, "role": "member"}, + ) + self.assertEqual(r.status_code, 201) + self.assertEqual(r.json(), {"ok": True}) + + r = self.client.get(f"/groups/{group_id}/members") + self.assertEqual(r.status_code, 200) + members = r.json() + self.assertTrue(any(m["member_id"] == member_id for m in members)) + + def test_04_property_set_get_delete(self): + r = self.client.post("/creators", json={"name": "PropAdmin", "public_key": "pk-pa"}) + self.assertEqual(r.status_code, 201) + creator_id = r.json()["id"] + + r = self.client.post( + "/persons", + json={"name": "Dave", "public_key": "pk-dave", "creator_id": creator_id}, + ) + self.assertEqual(r.status_code, 201) + entity_id = r.json()["id"] + + r = self.client.post( + "/properties", + json={ + "entity_id": entity_id, + "property_name": "email", + "validation_policy": "default", + "source": "hr-system", + }, + ) + self.assertEqual(r.status_code, 201) + self.assertEqual(r.json(), {"ok": True}) + + r = self.client.get(f"/entities/{entity_id}/properties") + self.assertEqual(r.status_code, 200) + props = r.json()["properties"] + self.assertIn("email", props) + + r = self.client.request( + "DELETE", + "/properties", + json={"entity_id": entity_id, "property_name": "email"}, + ) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) + + r = self.client.get(f"/entities/{entity_id}/properties") + self.assertEqual(r.status_code, 200) + props = r.json()["properties"] + self.assertNotIn("email", props) + + def test_05_revoked_entity_is_immutable(self): + r = self.client.post("/creators", json={"name": "Revoker", "public_key": "pk-r"}) + self.assertEqual(r.status_code, 201) + creator_id = r.json()["id"] + + r = self.client.post( + "/persons", + json={"name": "Eve", "public_key": "pk-eve", "creator_id": creator_id}, + ) + self.assertEqual(r.status_code, 201) + eve_id = r.json()["id"] + + # Revoke Eve + r = self.client.post(f"/entities/{eve_id}/status?status=revoked&changed_by={creator_id}") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) + + # Attempt to mutate a revoked entity (should be rejected by core rule) + r = self.client.post( + "/properties", + json={"entity_id": eve_id, "property_name": "email", "validation_policy": "default"}, + ) + self.assertEqual(r.status_code, 400) + # detail message depends on your core exception text; just confirm it's a 400. + self.assertIn("detail", r.json()) + + def test_06_metadata_set_and_get(self): + r = self.client.post("/metadata/name", json={"name": "My CA"}) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) + + r = self.client.post("/metadata/defense_p", json={"defense_p": True}) + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) + + r = self.client.get("/metadata") + self.assertEqual(r.status_code, 200) + body = r.json() + self.assertEqual(body["name"], "My CA") + self.assertEqual(body["defense_p"], True) diff --git a/tests/test_api_smoke.py b/tests/test_api_smoke.py new file mode 100644 index 0000000..8088876 --- /dev/null +++ b/tests/test_api_smoke.py @@ -0,0 +1,15 @@ +import unittest +from fastapi.testclient import TestClient + +from ca_api.app import app + + +class TestApiSmoke(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.client = TestClient(app) + + def test_health(self): + r = self.client.get("/health") + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json(), {"ok": True}) diff --git a/tests/test_api_unit.py b/tests/test_api_unit.py new file mode 100644 index 0000000..fe5f0e0 --- /dev/null +++ b/tests/test_api_unit.py @@ -0,0 +1,83 @@ +import unittest +from contextlib import contextmanager +from unittest.mock import patch + +from fastapi.testclient import TestClient + +from ca_api.app import app + + +class _FakeCursor: + pass + + +@contextmanager +def _fake_db_cursor(): + # Mimics ca_api.db.db_cursor() which yields a cursor + yield _FakeCursor() + + +class TestApiUnit(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.client = TestClient(app) + + def test_create_creator_201(self): + with patch("ca_api.app.db_cursor", _fake_db_cursor), \ + patch("ca_api.app.entity_core.insert_creator", return_value=123) as m_insert: + payload = {"name": "Alice", "public_key": "pk-alice"} + r = self.client.post("/creators", json=payload) + + self.assertEqual(r.status_code, 201) + self.assertEqual(r.json(), {"id": 123}) + + # Ensure core called with (cursor, name, public_key) + args, kwargs = m_insert.call_args + self.assertIsInstance(args[0], _FakeCursor) + self.assertEqual(args[1], "Alice") + self.assertEqual(args[2], "pk-alice") + self.assertEqual(kwargs, {}) + + def test_get_entity_404(self): + with patch("ca_api.app.db_cursor", _fake_db_cursor), \ + patch("ca_api.app.entity_core.get_entity", return_value=None): + r = self.client.get("/entities/99999") + + self.assertEqual(r.status_code, 404) + self.assertEqual(r.json()["detail"], "Entity not found") + + def test_get_entity_200(self): + # FastAPI returns dict(row), so return any mapping-like object here + with patch("ca_api.app.db_cursor", _fake_db_cursor), \ + patch("ca_api.app.entity_core.get_entity", return_value={"id": 7, "name": "Bob"}): + r = self.client.get("/entities/7") + + self.assertEqual(r.status_code, 200) + self.assertEqual(r.json()["id"], 7) + self.assertEqual(r.json()["name"], "Bob") + + def test_set_property_400_on_value_error(self): + def _raise(*args, **kwargs): + raise ValueError("bad property") + + with patch("ca_api.app.db_cursor", _fake_db_cursor), \ + patch("ca_api.app.property_core.set_property", side_effect=_raise): + payload = {"entity_id": 1, "property_name": "email", "validation_policy": "default"} + r = self.client.post("/properties", json=payload) + + self.assertEqual(r.status_code, 400) + self.assertEqual(r.json()["detail"], "bad property") + + def test_add_member_201(self): + with patch("ca_api.app.db_cursor", _fake_db_cursor), \ + patch("ca_api.app.group_core.add_group_member", return_value=None) as m_add: + payload = {"group_id": 10, "member_id": 20, "role": "admin"} + r = self.client.post("/groups/members", json=payload) + + self.assertEqual(r.status_code, 201) + self.assertEqual(r.json(), {"ok": True}) + + args, kwargs = m_add.call_args + self.assertIsInstance(args[0], _FakeCursor) + self.assertEqual(args[1:], (10, 20, "admin")) + self.assertEqual(kwargs, {}) diff --git a/tests/test_entity.py b/tests/test_entity.py index 304cc7e..24937da 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -6,7 +6,7 @@ import psycopg code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import entity +from ca_core import entity DBNAME = "ca" diff --git a/tests/test_group.py b/tests/test_group.py index 64c5673..cb0163c 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -6,8 +6,8 @@ import psycopg code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import entity -import group_member +from ca_core import entity +from ca_core import group_member DBNAME = "ca" diff --git a/tests/test_integration_zenroom_service.py b/tests/test_integration_zenroom_service.py index 51b8a30..f7fd704 100644 --- a/tests/test_integration_zenroom_service.py +++ b/tests/test_integration_zenroom_service.py @@ -1,7 +1,7 @@ import os import unittest -from crypto.zenroom_service_client import ZenroomServiceClient +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient class TestZenroomHTTPIntegration(unittest.TestCase): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 2049cac..79f9558 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -6,7 +6,7 @@ import psycopg code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import metadata +from ca_core import metadata DBNAME = "ca" diff --git a/tests/test_property.py b/tests/test_property.py index a472947..d07a09a 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -7,8 +7,8 @@ import psycopg code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import entity -import property +from ca_core import entity +from ca_core import property DBNAME = "ca" diff --git a/tests/test_zenroom_client.py b/tests/test_zenroom_client.py index 8168602..8043ad6 100644 --- a/tests/test_zenroom_client.py +++ b/tests/test_zenroom_client.py @@ -7,7 +7,7 @@ from unittest import mock code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -from crypto.zenroom_client import ZenroomDockerClient, ZenroomError +from ca_core.crypto.zenroom_client import ZenroomDockerClient, ZenroomError class TestZenroomDockerClient(unittest.TestCase): diff --git a/tests/test_zenroom_service_client.py b/tests/test_zenroom_service_client.py index ee36fde..236a36d 100644 --- a/tests/test_zenroom_service_client.py +++ b/tests/test_zenroom_service_client.py @@ -7,7 +7,7 @@ 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, ZenroomServiceError +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient, ZenroomServiceError class _FakeHTTPResponse: diff --git a/tests/test_zenroom_service_client_clean.py b/tests/test_zenroom_service_client_clean.py index 08cb623..7fb26b2 100644 --- a/tests/test_zenroom_service_client_clean.py +++ b/tests/test_zenroom_service_client_clean.py @@ -7,7 +7,7 @@ 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 +from ca_core.crypto.zenroom_service_client import ZenroomServiceClient class _FakeHTTPResponse: