From c6d4ce19069f759767fdedaebc2cb15560e4b3cd Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Wed, 4 Mar 2026 10:34:06 +0100 Subject: [PATCH] web service api --- , | 78 +++++++ Dockerfile | 78 +++++++ ca_api/__init__.py | 0 ca_api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 130 bytes ca_api/__pycache__/app.cpython-313.pyc | Bin 0 -> 11833 bytes ca_api/__pycache__/db.cpython-313.pyc | Bin 0 -> 1181 bytes ca_api/app.py | 213 ++++++++++++++++++ ca_api/db.py | 24 ++ ca_core/__pycache__/entity.cpython-313.pyc | Bin 6551 -> 6551 bytes .../__pycache__/group_member.cpython-313.pyc | Bin 2663 -> 2663 bytes ca_core/__pycache__/metadata.cpython-313.pyc | Bin 4107 -> 4107 bytes ca_core/__pycache__/property.cpython-313.pyc | Bin 4085 -> 4085 bytes ca_core/entity.py | 2 +- ca_core/group_member.py | 4 +- ca_core/metadata.py | 2 +- ca_core/property.py | 4 +- .../test_api_integration.cpython-313.pyc | Bin 0 -> 10876 bytes .../test_api_smoke.cpython-313.pyc | Bin 0 -> 1182 bytes .../__pycache__/test_api_unit.cpython-313.pyc | Bin 0 -> 6211 bytes tests/__pycache__/test_entity.cpython-313.pyc | Bin 9344 -> 9360 bytes tests/__pycache__/test_group.cpython-313.pyc | Bin 9636 -> 9663 bytes ...ntegration_zenroom_service.cpython-313.pyc | Bin 1109 -> 1117 bytes .../__pycache__/test_metadata.cpython-313.pyc | Bin 5579 -> 5595 bytes .../__pycache__/test_property.cpython-313.pyc | Bin 7017 -> 7044 bytes .../test_zenroom_client.cpython-313.pyc | Bin 5945 -> 5953 bytes ...est_zenroom_service_client.cpython-313.pyc | Bin 13574 -> 13582 bytes ...nroom_service_client_clean.cpython-313.pyc | Bin 2991 -> 2999 bytes ...integration_zenroom_docker.cpython-313.pyc | Bin 4311 -> 4319 bytes .../test_zenroom_live.cpython-313.pyc | Bin 4184 -> 4090 bytes ...service_client_integration.cpython-313.pyc | Bin 4166 -> 4174 bytes .../test_integration_zenroom_docker.py | 2 +- tests/integration/test_zenroom_live.py | 2 +- ...test_zenroom_service_client_integration.py | 2 +- tests/test_api_integration.py | 187 +++++++++++++++ tests/test_api_smoke.py | 15 ++ tests/test_api_unit.py | 83 +++++++ tests/test_entity.py | 2 +- tests/test_group.py | 4 +- tests/test_integration_zenroom_service.py | 2 +- tests/test_metadata.py | 2 +- tests/test_property.py | 4 +- tests/test_zenroom_client.py | 2 +- tests/test_zenroom_service_client.py | 2 +- tests/test_zenroom_service_client_clean.py | 2 +- 44 files changed, 697 insertions(+), 19 deletions(-) create mode 100644 , create mode 100644 Dockerfile create mode 100644 ca_api/__init__.py create mode 100644 ca_api/__pycache__/__init__.cpython-313.pyc create mode 100644 ca_api/__pycache__/app.cpython-313.pyc create mode 100644 ca_api/__pycache__/db.cpython-313.pyc create mode 100644 ca_api/app.py create mode 100644 ca_api/db.py create mode 100644 tests/__pycache__/test_api_integration.cpython-313.pyc create mode 100644 tests/__pycache__/test_api_smoke.cpython-313.pyc create mode 100644 tests/__pycache__/test_api_unit.cpython-313.pyc create mode 100644 tests/test_api_integration.py create mode 100644 tests/test_api_smoke.py create mode 100644 tests/test_api_unit.py 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 0000000000000000000000000000000000000000..2c0c45fca8ec6dc9eb32da6ed2ff2cfb35c5c99b GIT binary patch literal 130 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 KJ5)J)XZ=5bvxN@; literal 0 HcmV?d00001 diff --git a/ca_api/__pycache__/db.cpython-313.pyc b/ca_api/__pycache__/db.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02539efdb553ee5fed04e40b88abebb5654433ab GIT binary patch literal 1181 zcmah|&1(};5TExpn@u)pYAW@k#deKf4cbK1B9{7*q6b^)p=m%!W!Y|CVldr}?`_n0 zv3OIEqFD4GsCW?XUOdLXK=C3Af>1not3(jggY%lK6$E|D&iv*#^X9#snQiy<=z#0- zqvzEd6kt^v!3uQ%X*&p3AP;#m4O4^@Lt;8jIYq5ZD^npJ>T)Vqkqb?Sr!=k^Fi&SU z1?e_)3i4_`Jcj4V#iR3~yfy%19TT`-q3OtHX{CuDvmD3uEU)T1Vg~Jm?K&R2<}F%| zRbd>NNU3UjCU>v;@zc4v++;3)#w^U9yMD0l3a`SMxLh00W&>k90H84z=jHA1Z6QLu>TE_x^Tc?o}Ec&bU7F3G0S+6%bPRlu&>oc z({}3>U&8@$Fc+6S;3^6~Y&W=Yxv$zaSFkkoLo&u!BrQ8>82(`PqPxhlcqN{#U#j9X zm{z@-EiGi~O+Q*%Fgq!@oaQ*b!bI^Imf&j~;+vOG-ah$g;^q0bu@g(D*Ql!Y`#s6` z$^Hj}cL(q9c(UvLmhJbV-xTN@Mv>@Si$JtzS-+*;h<%ZYb%rv=zxqa#iU+?};&J2Ce9m(qbSxCLdYjbtwQtz>{*4x2iW 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 782ccd8f60c70577584b90982db0c15ea070e9c5..0f800defda2f10db28831571a4591e883bb724f3 100644 GIT binary patch delta 413 zcmbPkJl&Z0GcPX}0}$-LzdTcSB5wsFW0csuilvr`QEsya`$R@Y zxyidYEEp9xf95D5>p0>RPi|%r6!i7rYNKr<(C#H6lqM>7L;aF1SysPY8IXxFUT%{ zWB^D)d~%tf4x{eo#ey7+jIxv02x&0tZ@wzDnUT?CbEk+Jqmm&=l@XB8WGUhXv3Y>R zEza!J%J{OxoYK@Hc$h*Mmf(Q31)xEP}hRudq8FrJ() zq0DGAxmzM7NdjcJ9EbqBSPjI|0uj0(LLWq!fC!LHMOGlf21u-AC=vj%`GCYN4x8Nk fl+v73yCQoaml24IJ%PjrW=2NFy9`Q`10~%6X|!5I delta 413 zcmbPkJl&Z0GcPX}0}!xTEX>rI$Xmh4u(7R}nW>0r@^9vKjN+SDvD7j#%5K(RpUB84 zJ9!s}1*5{|&m5&pjM9@cxkDJ`C-37{VpQGyklU4sQE9R|pC+TiaI;$B_bl&8C3F*laVjqA8af)j*T5dKK7h{ycY68R$Mw8Pe zlo_ojcT1!siGvK61rZ7$LKQ@4f(RWDp$8(2K?KOAB1;fq4J1}F6bXRXd_dwBhfQvN fN@-52U6CD-%Lv59oBR|R0_2K 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 0000000000000000000000000000000000000000..b2a50f615e88f76a29b029fcf17ff78c78601a9d GIT binary patch literal 10876 zcmds7TWlN0xt_g|%bO(1x?8en%T`2366Jg1I7%g1u4N~3$|c>TGP0q_l|-8&nc0<( z#fMV^1qwMWU<_A{(a6lTCgZ_74kkO^wVXTBS zMb<*&F-7fFKZ&J|7nkzH6Pia=w6{F!wP-RKjZb<8-XHNqonK(OEYv%du z%vj;~K#_cAA1sP$b&u3Y5^ z%+>_!LG8!`f1{0WbM0t?sa#?b86~`yQB0Mc%r0mj7fL2m!i+Z2^9H0uB_2*hK!;%U zgtLR7ggTp0b*mD;9>s~cZV!j#P%_#Jl`xrZOjbs>gbnR=4f%%$0>d(>@Vmp3ZUe=Q z$eMCP)2&H$Hk?RK>eg@~9#_H|)S1C*0z=a`l<iiK2FZ`-Vsy@l6g z@zom;+(chlQ0MDEW^dbX+3)mc!~=h|AX~@()VD8kcjYR8mW?)v+C=lA}kZLuR;v**5J-m!3DwdS?Iaj3Q(f~tdQ zSI3{+`|h>9q^YN_j+g2|=={-M_n?P+)LlDREj;QMA^y19KGg{3Tk_|>C1^hPfG+31D^m+WQ<)&+C*NerQ>4pmkoDV2UPDsKe{~zaMb_`y zT-Iz&V#%7Cy{jz1{)TU0bIw6jS;sGNhK}!}trUMtTYWqEFM}IRHA%zo4i5!9LzjZ1 zfv&?|&pH34_dK!0B&MH>6%$Or^&}vp!h}mO`SNuRbWRs16fHcJh$~)B=T#*(fq@DU z-V1?BFb-4~6I#*jGPEg&6Eje$oJJO2A0Txed*Svg^?9!=Nyw2ozKLkjM%i`{#o0Dwk2*=>@Lo0c+l`!)8g5sgNtXLihb+Q zN(GW#1|jKE0YH>K<}}eoA%iJ-$}N<{5DJ#~UKZ$+jkB;UWxxXfLo8_wl9Hvc6i^~h z--VKOKa5a-5tN(x z>cX6uG07Das?f2#0kOlmuPU02E$2RNsj#LNV^xAeQA_1l-y&6$^^@$|C|N^Gx(g-Y zBdr~MY4cGtIDu7N7>Gr~N=i&#>j{zA=d8}hL(__GPtJ@(hI~z#{hYzw(Bq5$hCpYd z5lqnJrRYm4V06S)of}Myr!3IIcw+o>4ii1YCrMd*t{XWTd1?27Ngmarit59}*rdgD zUYkuSy2WV8D`G;}23+vTgsS1gB>g%Ghy;4A0UzI{Mm(k{g1zvWShpEHfFL?Qo`}q<ljv_% zM4wkzCW5KG@Yw+_fj!ec-@f8LFlTvgVAL~bYsT5SF!s>;B$e@fFYRniJI5;ERei(# z%k!7-ey|Xk|7gy>W-?9<(i^+dUdIJ$&qe7zT!MlLCor! z+t+Vh|Iv*(zJRcG3y}vciyy9vzT&)z2d#@Ut71=S-VMND+z0cvfi4LGQo6hf;YY&G z2y+9l8NppH%j9Eu9t;Y{8L1ieB*Y-U2bmgd%4Pm~5U@wfC_V}+1H2KL zj>Zk%ATfL#a`7n=yg`CDApm1=65unb>W<;~WHhcQ7@T3s6bk%;``d$+iJrc`BVNA1 z8U}7V!8eAN3I-oq!Ka}$T^I`CM2vPBCNX@5^iiGb!#Ax=j{_m;79#>qgGhTPm~S)k zDEvcB1rz%)kP^+h5X)7k+_V8{2+ACT|8+9%ex?I?sLi(rop&OXa7&1AY=Av|9--^# zp5Q_6!PbZaN=-7SxCsVK+V`Qr=;sI|Vte~^W}0T?T$q~%``FG*DskmT68G!=iu7_! zHU^a2!wN+ra&BC)DA>oj5%ivkCBmVY+TWXRa|#4dn;`i63m~5K-jTmJs4ez z{;u`Z#Am%0%De@}#h0L}ZZ)Tq zj9n%60;Sq-8KdnsxORKv(ck%+6LOA-FbACb_q;Q(d!j8N()#C3hqC)Rv-`TTp6)Mf zLTy!!Pesb7wPpEqPu=~K^C$1Vv2b?2e@-lE5vY0pa?6v`8TaMgQnN9-tg{UyTmJBg zm~nq+8%-5CMMVzKs@PXd_*08F7OyUg>DD(_oTn>Le(j$7o%5Y{yXLGw<+rPERsWP* z-8=HAab@qwO4GgWH zmTGs&n0qK=I(N;Ob=zf3sh)bs6?hYTMZTz|@|H@TTMKI>`H-e zstd4F9*ybNh%ynHiD@a@6z)-HRZW@p^16+>u(QOBaOLXE%G6tSUkC3wLJx@KWCHx= zS>2*0W-!<;)YQQJpa;~p0LQw}o8>I*kOqdqMhKJl5eU4tS1_<>Fz`rRw_@d!8Q2*! zchyD{+DII>+d4>Fc^8;AtqY`AwXo@L5OKRf#K-0QXkhnVhK)KoqQn$U(T`T--imdu zLhtIIfNnRlts^acFXJ1de(jYC?6J|W-As<2W!saMj61LmMl0*Ln&jC0ysb04zbkvF zH`{V(yUe{Uj-*cU@>}WFcUGL=sxZnL_kGK}<*vBUnyKle4*PA_E!UmN1@M3mGQ&5B%}HN;cv!d}+8_y%RnZ4ERo9(pQvZDw(@r$Vgd^(Av( z6cVLRSx(xx@A-}TmUL&YR47Q7l`HR&|0}XMc#p0iTZNA0PwscfD8;*$yJ3`Ssb(8Y zPBfzAk-%ps9=lYxt9h;8z`o^u>n*LOLVaM7#vF@S{mS*kH3g@5UB^i6$k(;kP*ED z0IFLeiUw>1K2}AiEJ|wgC6KRMKsI10>edyuI6DlqKLL(48EHf|=}eWQ)6+BbPK184 zBI0jZ?OnDSbpr2R=$ma=Pu~6=$e|OcjB&q)iIXhaOtfm*tH6<5%<2bMT4?*iS7o zzqI_^@(b~)=-mva!~&&YI=U+`?PDZ<#~X+n4=F-idPYOBDz&Fj7R+KLt}1d;x8ndZYwXP=5kD5PKs z#y{$<)CFJQuWG1FS+$qcCB%Wvhq^f0kB8pWl23xi91Y|hEFDJXy>=o5a>5-fQoy)) zmAi!n8VC2Z=*1K=c@t4OVc>G9iP&|8v;|w!nyT~uOP2z;mh@yNcLsxTxLBy_B2m_% z5L|YkgCxsP7`QH;F_xeoe+n;vHIe##2-Y};VSbNnzem-7L`{D|RewZYawg}vZTr-A zr*}o%e^dCg)iHPe2mkrZ+Lp1ltytTiSzpUoUt3}_*26dXY)xId@o=W*@J$zx6l;IR q)cuyJyAw>e`d1j~EA}eO99?L7)_fS0WF3(dOfwr`zMyZYfd2=a_w^D0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..84d7f830f322887b1dfcf6d2087a961134285591 GIT binary patch literal 1182 zcmZuw%}*0S6rb6zu5A&J0JRCRF+j40v~XzRftVVX@ka(CWtTMGJ;{mpyxzURGd4-6!cpz*W!u8$G= z0YaD54%03RriDyo;u4~8N+rA~Q7H$!T#^?Rs^m}x-9)C6MW(unmZPZMK=E0Wl$n%yhj_80@79G@=K{JK+UGKUs|4Yv@AT3Cd{3S7dVrhM>>Cz%sgf(6fFjF0yh zoIh9SA|e|Qu!(+--gqa!CR_5$WLsQZ>ZN>S?d!!tSJ>c?~;RT|M$D{uU~g$rfe#+_&kPqTebW=QK&hV7uHTb2~RHh8!LuN zAqtnPPRLW@sT>+Uc-pi)o?@XDHW{(~HHRxtnBOoYt}w@2=a@1mE|;Sz@J+!4NF14G zF1&P~CM`_aJpr5OI6bse+Ai%ZZ7=OU{G6WN!pD00NY5PVncd;H6MGW}@)v!k|88t= z>`i9>{=ww_{8xRJUKL?Jh!xklPDsP5JB08gA$5PP=>eZ61bXOb1_@brDGNQf;WT`q zPT8;&btkO(YZU6D83BBh5a`*pi4}&_ecTk)6GF3K02!lhDGVzDSbKI|zzcM&4*iy} zI@OkuHhc=)nI@}qu@^WP39dIAZV2zeudQ1mPvGW3iZ?vkd7iW_SmC_*QRDsBSA@la zC594Zy;up?=i`R&&?%7ueh!usa4KVrzoEhJ$oPe_e>5G>y}ErgGI=;Md4_}#RrMG6 C()}I) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4a0d7f393603443b91a814154aab93d126fd10c7 GIT binary patch literal 6211 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 Mn-JqydXl#7KTxgYO#lD@ delta 960 zcmZ`%O-K}B7@lu;W`EqBoy~B6W`A^D|HeWhG$pAJK@!o-l@1{+%Qo23vZL=zh6K@} zP78gsZq z?FvN65w07HWh8}FQ$)K14=UaY?=i)G(DIqFL4GTmj0|B6P7!4IqiBxB_`}w=exii4 z2u^rO2h(W$rG<0oG$jNgB*x#-56JjV9%mk|eq|tez+(E`0+5NZmoFEx_L{QeLkX4E z_Vz;(Fb;DB!vrG)t|0Az$_Xukaee8(dhGYD8HR#>9)0N*^s23xr!m55f>DAq2#1EV zk=Vsg+e>`(JmhV%kI5UUJIHxAHRk1flg=@Xe@!O>`!Ac#s=Ql$hPm771RMF+%a*Bg z4MFp}Kj)4aE|U~m+2&BEvM*4|wwku{xtrdePx2j1wqNI0d>)w-g>H1$3mS{(sY8*V zpJ0GL?VO7%bS0H=#04HKu0~uZ*;<^h74xi(KQ9hyuIeOZxoa1i*G+4@f;;C{-lIU1 RjYqo{{GS9|_pX9Su0Q%`&c^@% diff --git a/tests/__pycache__/test_group.cpython-313.pyc b/tests/__pycache__/test_group.cpython-313.pyc index 6861895208ad8ba8a4533f1e45e1a4e3c720a765..506f4cb1cad8a2b96cfcff21b4994771bc9e979d 100644 GIT binary patch delta 998 zcmZ{i&rcIk5Xbkm+ihvPbPH&I*}BkHS*l=zA}C-LQGo=EH6Db7l)6G~VRc#wBpRh#upOSJr`h%p0PC^%=A=s9Bq;ARv&8&OK$p*OLO`<98eHsE6 z7Odvd5?ZOv7C|r641hSF@~JM6u*M(y&M<|)_8npwzUk`@<}g{Mib^X70kh5D&Co&C z9WiI*$By_}EW#jR8Zik&NX-eulsU(l5|IKFp69BvdU}M4O|}iVNNIW;xI~~KaG7w0 zFh&?hXfDX`_eut@u&w;~J6UY`!dO55-g>yJ4>gt>^kt*0-+}?$)xG-;80DwhvR#ca zbrCW&W0EjM$RVmCg06};U2BUvamb5Ms*{GmJb?zl0-=J~CC8nWpMGu2xAJfO=lS+_=I;_A?D08q!P#zm+5gq; zV-g?hXh(6mLt$))KS+)#D%ndDjuB4q&E)U_`m_)v*q^kOqE_CSD*5ar?X?#EB$Z$x x{w8&@m)>&c1?-DPXsaMB&vm_88!6ylX!qFFm&emH(v}eYAgKINx?QBke*hD#%}M|O delta 1060 zcmZ{jO-vI(6vubEOIy3$c8g$3LDJHKZj~Ah0`eu_M4LcE#b_XDN?DB8H^i1DB| zZwBW^;=zxYaPiV`Hl9sPypAVhVl*Vg#H$DAtqD{EbNJ=WoA>7b-h8~vyv``U6vZd- zal7|L>G_t@ME1ANyj>s}8RVRhBN;#Va}p@Ix~Kqwh|nhF+)*LtSrBGhG40=RS^PT( zb(v`1A2^xT2$Qk}dxd$5g?!Njy_zEtLIPF8^_>uz&Pg3ap%0~X5~Tam)6>m7iPaSi z)6yZrZD`@J2uBPQx?MN##5K{Nd$2vuy*n-^jCsK{jKxB+yu$@8-MDj*v+f=~%sGWQ z(1AG1aS+t%x!^vJ-XJCQO;y8_6iE-=@+Ek2yS|rE=GH5A$=+mvQiWG$=Zl~hY!jfF zR^(WK`Io`mSj(^KtBX2ND)*59{V2B(jqb_m&=5+wIbt}5L8+$vX~L^QEl{4g2CkrX zA7Y+k8u~dk5eCpR$id{|O37HtJ2DoYb1e1<6kNVayVcE>5pHTHU=%%L2s}ubKuppP zYLr*R_3bOVGvFnFsWH zW?5f?9zNUg9VswQGYuD0lc+U?Q4(lqPa|d!PX7|OuzlYBn}(=Jy6O9HQbrM2M)c61 z;V@CE-pCi%EmYII|ErB-++9Q&aiFNxDxw(koA!zh`8B5%t44aHxh!VA7$V<49LJp8 zae83z4QhA&-8w49otlrn0%xO1ns1Bq1|PSng!I#AiEAo;B1j-oh&0_x3^n52LlEKI zZwRdr9ZHr`juYovh^m{TR}zOZpH$}Nz%u4r^o(da7!kW(ZC GR0jaUt`AZG diff --git a/tests/__pycache__/test_metadata.cpython-313.pyc b/tests/__pycache__/test_metadata.cpython-313.pyc index 5e7fde918090938e17ef02f9d182c8fd1adae965..550202562e492d183fe9c6ed0c617e14804deaeb 100644 GIT binary patch delta 663 zcmY*V&ubG=5Z-BbH@ohSyu>7A(@k1RYuN;UAnn0}1u3GnBI4>ntPM-@jAA#Dw+rH5 zA?5uCUbGhv1`mRl;!!<|>p>8DsCW=Ocy(s0t$l}&nQ!KsZ{ED`eCfEq9OsyV&;0iM zfo*QNb@;xu@UpM2Syyaf&nTfLoKSBl!mTT_N?2$pp|P&qXp!dVTbkU@1sgzM?oo0r zfK0s}rT4Q!FY5M2f+JzXJdOHM8jS;(0w|6zPJ9Kh<-B?VTzOSZfyo{9Y1796nbzAJ zC0uwIizY$>2s6T!v9^)-d2&}fZ_FaQJs~{V)_ojuNnbfE)~F~J2m*wI3BUET`blEZ z&h*BY3mZ@p8WNe2@U%OOVlI67)|m2&L~5I($6RDal;A$;>JaVP@~2TlyMp-!*m%bb zp`Gh3()UJj%!jF z*fe#_5p)Pn%U0zaV-&gsvR^%7c>pSZP5`{-`XTQmG;Ls#BC-&$!8xbS1*m Yrp*dHlZtLu}yiZ5SOr&Xf<0KF=h;Q#;t delta 747 zcmY*W&ubGw6yBF^vfFO=M@?eVq}FOBb`yz3n?nWZp?Hvrh)EGKx(vw}#bj4!S4t1X zKS9bIy{I=Y9*m0rgCcm7^<+o<+O}cHX4h);T=peee6e_vX!)+WVUI%QPnxah^7w z-TkL;TSfS>eez`s)(mDeRW{j7mlS3emE%e?Q&O5*Te&)e{Qpm5Y?L#*2d@4Tlglo| z>J<{*iM0;#Isv1@Whet!!}Zix0E0Kw3YfgDMqu+#>fl-^^%3QwFQ#x2AMXb}kNOeY z3{xVX6Eh>6k7yWqXixmEmsrKM7UhfMPsGqG+Vs$}3&8(Q%vaB%#6Wf@7!1%2=V1*A zqd0*rZdxVr@UHP{*JE+=IXF@;A9IgxhOErLnN>)*3)Y9deif)ou8F<6^fvWF>fJxk z&BSi|g~Vgad9&+rY3e2z=)smSl=+=q<6^RU*DhD%Y_va^u(Y?XmBQ L&EMsZstD}>kfyg9 diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc index 73d4330be8acb003e518e7f532436f4285759e19..77e8f31a10fc5f593f7e75f2f13f801d908148af 100644 GIT binary patch delta 798 zcmZvZ&ubG=5Xbi=yPGu0W;YGC-A!B8ij8YxFlmjVRcur0FD&AgdXa>XJTx_A%VcS3 z51vGi65k(Dym;B0H~#^Tfkdq)q~x`hwq!2H{Ulqht+2nweOmiknkJ( zdeGS)XeRqH$iF!!-$~w1LCTg~$yHEmprKYF?Zzw#^eJgha^sfdjy;s_+k;=ZO6C!B z5{J}i-C;xdUekX(jJ2ALmIoXID)0Fne-BlB8@z1}{$5aKImV3OZsa3ln#jxZOc(32 z&obh@Vv0}l&ddz98!D}?*XLlNhFL_PA#Bivr_{sR&G;#dypDh+it02rxuw=di3v); z8Nyk_-P2%Z}U5mptY?0n-ARb3|hQ&;Gcc6@)Tyhd0i ztPpC1Rl+g8L6&?!WP3-W)lV!%^5JMP;5*(E-X1D6)Pj@v&r!UaEIahmMwTurCkh1$ z+#uXU^rHyd#+3%Ssh7;KDRG)mB%Bv-%|#v{TVRq3*A?I3eZSVkC5Jt>C^70QzF2kjn`C{Krp0RRq)gF2bfK!l delta 752 zcmZva&1(}u6u@_q{RrL6ZfeZOCT&ftMz@4wB}A)@C{-&KthnMy8e>@6#AP?W9hKa@ z)>6ikD0mRWi&XZn@a9ciPvWT{Uc3lioVS97(mnim``*0Y8)oL|ncWusMd_F%zBl3P z{=IE#k)PWq-XA9`I_Qq%kd-WC9T}*jR3tFU(v0M&70J==O1Gx?LnD7Ijm?q-xpiV^ zb}_tRlUTmlWgD^9>vnnpxX~)95@JLTQeOzs`4zcNDEDMWO#V&YnyiYZSm}8|=t3D? z69OR-r~&1h%389{;)>eNOpENh6qNaObwWti)up{+5sOeqoD}$PC~2qF24+pZleMA^ zO-r|8)%97Qjbfwki?OWJ1GmcpfMhe&^&#-Q&d`N0@Z-!70uizie>FBR4WWt^FL>gD zt%A*q`X!ZO&f?E>D|)Oi5Mo9<+2s^z^54dh!a1CM9)WULM6`LsY#c%rTt!?IuoYn& zm@|Y%o2G%1m$|nnxty<`YT>H0h`ne5n<#cg+-k4p;XN6 z7xB-fdAp4>FCZKomj)Z-WyCds5FbFTCT=R4FK?0D6c&ymW)XAzlhw?b7~&bhcmjU7 za%VP4;=er}XCC=|#@vv_>2MUr+E6?nuYVs<4h8bI?w?)fRS6pD? Y9bn<#EdHmmrhb>`SIOj^>a>j6zeV(=umAu6 diff --git a/tests/__pycache__/test_zenroom_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_client.cpython-313.pyc index 3b57272b604618fe50216f3b50cca28e511c3d84..b191fe311a8125036d7f175ff268289e79b71fc2 100644 GIT binary patch delta 42 wcmdm~cTkV_GcPX}0}yaNTAq1wBX7AFr)+X!d~$wKs@~*Qv004XlNH1z0Up^6ApigX delta 55 zcmX@8w^NVzGcPX}0}w2CSeSWmBX7AFqv+&bu~|~iU)X#Z8RceVT$j_oD5w9G0mLg3 H1*!x9-tZ62 diff --git a/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc b/tests/__pycache__/test_zenroom_service_client.cpython-313.pyc index 6481604d6fbcdbd515a9c9516a923029d1ad7a87..0fd43fb9fdba6302bfa892a6cfe7b34719a5d0ef 100644 GIT binary patch delta 42 wcmZq6>dWH&%*)Hg00f+mmSui3Mw!IaoyKmPc3~&9^E4?Qpm^_urd@V3D(tY+-gDEGYL@ce^Y8n<-|zRm z&q|>GyIzQl4kmcBFRB-(7;l5$pXN<@ zi*J_}o$_>kc)(Q5;^WQXwb;jKIS3%6zp_B!q;{#$j?MR^Sm#2XlBaSd@xl&0YEZJ(6w( z;sq zXozUXVBl%uZko7@A-T&|Sg3$%Y3KE(Rb2r{>{jFA6glqd+DO0ym<08g0S;I$Anh+( z6^PUx(z~N(12Z&gHQ+*gXTUDP#iaDUVfScdZx|b zsGPbpcYCfQ=X^OAsAGXP5)2*Fu1RoO^hneX-D%b8Y#<783}4 z9bw29hSt+hgu~BLnY-DwY$rADr^W+47i14^%=+2!K*Q8}C{pQ(kcy|J78mwWN%hXs z?WK+~<||{tzSo11(T#>bay(G?VD;otte%uwELIogu7tXNt$A-y26d6wzFP~8%M{%j=j2M?TS$qn~>E*!T_+Ia&GMmI7h6~Z5qE#r?Avosc zlk?I7t|R|p&gb4Q$txEwgd9y@r@wSxP-G7Uijg|B_B*Yu-S7(S(S^r!`2{mY?_FQ) W>>Ky@jc-!;1&c9@^mBqoCjSEzmk_A{ delta 1822 zcma)6-EZ4e6u$OFlmL+;e{CXW!rP zepgiq)`P(Sg6pmKzc;Es27h6h5$7@6f24-kD~zy*SXltqCpa>9gE{3aacbYIi1<@J zA}$K5-+kpew(R$?Nmf^LQ^q<@VX|&yu=Qv3rw7~w%XB#VzO6PMI&g5;hZ*M!;ce#u z9}BVb%n(akBcz~t=Lh~+ggX>MbABfxM!T34>w}~lVj~yKGi6>*gh_FSa~w_Vmb}U# z#z~4@1utiaY2qkokym}U?ErlP>q0LwCBEQ3cWigk5dX(U`?8qt(zI_a+XcpKr?=wtYMXPc?+-Hayf!6iyn%i zkQnXSXT!(&Km9JfKnH{W02E%%_$0KmJ_GX!s>vuE-C5pVu7oB^p^2*edYxm#fi12k zA}P2N+z#Gd*&Qv5Z&XFOvC8eH%AyLZir85aJByj!MCo|)VYDm`9R%kxyISe^sfWE~ zahMi{x5GuQI8|y_%VPgQV7k=a|41AlQJ|9XqyjEN0^AJ^UxesaD8-nJb59sJ20Bi0 zDgHcXvMFB_UA=^m$x%JW&n%odEY$|iA+yl?FWD;bX7E6#)0YuybME?+GBgs-vnD@} z=E8I~IDRhT>~Yb8zm@&$1TBiHbo+zn>3iJFkj=ae^3K75>x)?1FzhAGUcy=)WU;YI zvWMnj3^(&>!t*MGW{GoN+9-%E1eJHi%o5HlTWdKo1h4I~#=6Vf_#+zx7qS2o+i4Gu zjL?K2-SnE_?2IZkwcm@Id_nqNdYbZ01D$lM8yNrql0XHp8g&OL;K^*0c_a0OwQgH; z7REnZ(^p6s3RNFT(t0Vsq8lb<y)}kk<(5o>r1#f0rlv@N^Jkw-# z&CJ@w$h$9}ng0eDhQ`)+U>YM6pJcYAKVpUUudm&^R*8+4Vx!e)q8jV0#s_MC6dz-1 zh>r)hq`Hib4sTtq%13slx2KEUWw{Uhirih2yLZ)Z6A!tve2(tUZqF7c?@#?Mt9yPZ zsRdBHqX|A>F^7YJy!As>Ku4A>ODCY_9g!6Y4?pW&X=Kg z{fz^5JDvT|&A_A3*6%tt<*t@2}aAIGv! b#SvzVDK1rxC*eO)M|4$}U}LYb(V+S_S$q>f 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 3b8520b5c235fde42d340b2f9d3ed2d1e23dde2e..0d0714b52b9a8dd297d3b6a8bb11343c8f6d66ea 100644 GIT binary patch delta 42 xcmX@6a87~uGcPX}0}yaNTAq1xBX1Kwr)qLyd~$wKs@~)Y{Bs!HChH1F0stXn4MG3_ delta 55 zcmX@7a7=;sGcPX}0}woYwJh`EM&2fVM%l?T_~%I3ePQ!qWRzK 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: