From 7470cf71894a7ec69b94bf86076be3a863f8af58 Mon Sep 17 00:00:00 2001 From: "Morten V. Christiansen" Date: Thu, 26 Feb 2026 16:24:26 +0100 Subject: [PATCH] various --- .../__pycache__/db_logging.cpython-313.pyc | Bin 0 -> 414 bytes ca_core/__pycache__/entity.cpython-313.pyc | Bin 5906 -> 4972 bytes .../__pycache__/group_member.cpython-313.pyc | Bin 1951 -> 1068 bytes ca_core/__pycache__/metadata.cpython-313.pyc | Bin 1926 -> 2451 bytes ca_core/__pycache__/property.cpython-313.pyc | Bin 1279 -> 1589 bytes ca_core/db_logging.py | 9 + ca_core/entity.py | 203 ++++++++---------- ca_core/group_member.py | 48 ++--- ca_core/metadata.py | 48 +++-- ca_core/property.py | 39 ++-- create_tables.sql | 45 ++-- tests/__pycache__/test_entity.cpython-313.pyc | Bin 5177 -> 5164 bytes tests/__pycache__/test_group.cpython-313.pyc | Bin 6983 -> 6805 bytes .../__pycache__/test_metadata.cpython-313.pyc | Bin 4907 -> 4303 bytes .../__pycache__/test_property.cpython-313.pyc | Bin 5578 -> 5353 bytes tests/test_entity.py | 64 +++--- tests/test_group.py | 100 ++++----- tests/test_metadata.py | 50 ++--- tests/test_property.py | 101 +++++---- 19 files changed, 347 insertions(+), 360 deletions(-) create mode 100644 ca_core/__pycache__/db_logging.cpython-313.pyc create mode 100644 ca_core/db_logging.py mode change 100755 => 100644 tests/test_metadata.py diff --git a/ca_core/__pycache__/db_logging.cpython-313.pyc b/ca_core/__pycache__/db_logging.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0fcc75a537b4be75138aa9c8832cafe23e33e6e GIT binary patch literal 414 zcmZ8dO-sW-5Zz7MG)4;Q!Gos-KWGG-OTCHcp@)J*Xj;7pB~6Dk+9qXp1GXnU=^yC5 zSAU5Aq2wg^2MVEgXG42&VBWl$owtwOyjryg#?9j*`o{XJnr&zsk{=+M6OTNRhUU>D z@?7FH)NDPgj6n!LgkZa>6eUa@Ge1X|lP+mfB>%#rUD}|R8yzF+y4hJP7QiL**(eDa z#F9^06w8FkArvF&_eW5l9k*Ps+3v8G+qp01wPM$154U&SrpIch!gg(%Rp1qZRD!K# zMv!umaIOr60>-ivXPAt^Nzv1pyhKjm_kx6jGwAowKaAqAKAC0~@?J3X;}CcSh3aYI z5aEsdl&lZ;-Cdd&%bmSXYhBjMMvf=MMMqsoUI@MjH*v-dgv`7eCxdhZ*L)XS^)X@} SVWm?_S4No{3w=$n6p}wA1zisS literal 0 HcmV?d00001 diff --git a/ca_core/__pycache__/entity.cpython-313.pyc b/ca_core/__pycache__/entity.cpython-313.pyc index c3ad9887b4a9f3ba9b1e7d3b6c845775f3061bcc..9fe9993a1344f3597b1301e9147b1bae6635b3a2 100644 GIT binary patch literal 4972 zcmdT|O>Eo96(&XMkCG+-$ad_w@i=f|D{-UzI8An&WV`ECYA0E0Yf4@hIcQ~xk=e+S zF{GT8c8fR#+9oK_blV1-Q{kh1^HE0~ePmz<5QwxW&|Z2o?40)0H$zf?ws+&h0xbbP zQ=B(%hHt+2z4yG|(IGH!os`#a4|n{Bm=3dskSk=9>BUq&E2r({ z$Pf=V2lxITLA+oR%q$BJZI{F**h%)PH4hOjGNbT5{|TwVeUI^1&Oq<#4<|dk(}2Qt%T$+6jf8rR^ij)kxiO#5E zxi@MqlqrCjd_gl;G+gFP!(}d64q0lj38G}a4ReND{z6U{HQDeykhSz;Auk)kTq;+T zqZk+P24}d^MXVOE;Wa-fsbmZe7apm^qX;1bMHUN7a-^urII{duiKJ7>bOFl|Q~knn z$q32RDM`zKxikh)(bH68@UY743XIeDd+~{=_uQzID!Cp0ZcXTX9(fko7EZ2C)!f39 zFW&@C=mQtG1DABqC3x}l`jcP3^lZBkeIMBlT+}@mcR9v0vJWPibn}_J;RVIb-{9eB z+{|}RI^GpGl7rwP&mDn5E&&11Y^{N>1fVIw1sKVdU<1q*zG+2nlmQ$?g()njv;sDP zu`I(0zuq8AD1QX^V1W#0Jst1UMji&4^NTHjZY*c+7^;cs{ix zPav(d405@+kWU%>{xRc926kb6T9Rz7VYGPzXVk{~n24rg;_Z7g5GRMsM<2(K z7>iHCZt80rZp&%qe(GTeLFma@-uGuZ+PbC>`Jdx#+L3G)~jfH8P7YzPgv8G>a8 z;zK1QMcj@9L^q)1AvUz6rGWe4Wf$D)5(ogk#{&MYgZKI&41|F08GPH}cDi;uneG$c z41Ybm-F13x@>SRP>YZIzyLRYLztla$RR3)^;|xAsff(5w-1JxOZt>L>yMr| z&iCR@STRj@XJ7TOFzNJuolNJsCQu$qpAtLYo!};0_39LAFTgN1&068Y-zRI*3&Ma) z@DK((35J$1;2{j0V)-xigKGwj$s_1U09%ee4w8?7X50;%FLy@sxRA>Me$0qK-v&`J zpOJALPa8a?xOM3}hoAT-FA(l*KJ4e<=Uv-YH&itDX5R`=1!|NwEW7Rv= z8+!kzJNz$dzL3s`{_jxo=N}LySzIVCTXc79kOZa(Ljy*r6@&-i%m#5Y}Yvs-1>R{Ec_kXg( zi#1=*YRp7HbJJ^YwA7MD&fk|4u8Am>w5mdvk$g?LpncnkO#dmi+<~`;NiGY zFhA0a{+42p#s}m=f^IVxx*FLx>kPZf9|^I1u4Gm1CVEKMkcK|PY?7?1?E;GB(=x(l zdoj_{Qk+kG`tgVHsEs`-TJJ7MBY|4ligK?cYj*Qdcwo;d^GGY8Z+Wy(LgEJ3l;DV4 ztl|ihBfunlp(vsqTqe0tYAcGm)CSVcP6bcE3>j|q28dPWE!0H=B)Ofppse@&tojrE zqr?tB_r^?M7uHMb4=bU{gUWn0zIASENbjF}&3|_A;Og|10ny?3uuR6IURKL>utKRsI6wukUX zyY6LBD-l)l&_pEBEpPR!9X{UNy8h$L_|q$T5WVt^==|mN42b^`ghmb`w9(NV2F_E7 z{Jq#T%iKdy8EoPJu%zNiaOPv;)guu3Ud`J>dYu+TUaNRFzpRc{%hjCT zFTLjD)VX5VZwQ%%B>WpGtK_q^c{AyXv|qH0rgaoepENH}{-HsE$)VECY%tWAnRY!l zmI|3-PQHb&!8qxZ)SDo7IhJMr&W!B21oqP2-~ijVcRj%N?wt;>Axk*eAMFWlcAnh} Sx!F089D8No?}iYd=Kl*)azHEq literal 5906 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 Rtz*PQKLhiRU%)(!^)Gsusi*(| diff --git a/ca_core/__pycache__/metadata.cpython-313.pyc b/ca_core/__pycache__/metadata.cpython-313.pyc index eebe600239dd85de3040e8cdddff04a25cd0977d..c13f1fa75854ebc06ead45493db3e41dd7bb42a1 100644 GIT binary patch literal 2451 zcmd6p&u<$=6vt9j1;#4H|1H-+J95ve5o&(z1MjZ`K@KiPcrkd}>lzL2lyn~UpdEw!1U z>*>s+!7Pnc@?}G%pWolerZgIR+f~5bU7wy`eYi2IKos1zXY^ z%&an-a37OzlT!OyVyjv<5)IQ}iQ3arLeJ;)Dl-!Oy~Jy~wrm>p91mh)2qMtUTVS4{ z4pDwd|CF{at8KF6-E`y|ozPe-coGl;(us@$ms&g4%=?GQ!*{Id+ec)z2cbwGRhaM3 z(f?~0yr%vGZ6U;KE8ew&vpD9B^YCu7SQGdwjsikaq>Ab0f(EKT$WTWtpY(KN{o#6s z-e4gJI1pG6F1GxQQP;Ps6$2(@J|VK|cb4g(}RU>WeZBt8?7r6j!&~WYx#jJO!?H_tq@3v|j+@DlvLo2IZ~* z`Cjw5jVcLIylKMY$>c~u^Ow6*h0Z1Fu|mCeHd?jDlkJk8dur@1(;6%7KxyMQ>L6@b z`<5xxzrlR-$}9Xqoaurmx`$R7X1Bv5*-fquY|A^zoJ1$~DokI4Zt}V^a*{`C-LW8s zBF8dPrYw2kBrHL}8+Qxdbo_`UE&zm!9)!bZ_kU^z^=<|p@*EDwIfugpY#vkBf(>l{U9&sXG;DPRztQ@c#?zvC&f~w=zOr9Z!P1W6Q>|DUH}+p($U}f#RKF- zd*I$JR|}18V~ufW`FS;YsW=uf#($uNt|Z|*-ANJ0x)BNA$9(v1Hxk0{b>9$iycZF% G%j>`7#@7h| literal 1926 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 diff --git a/ca_core/db_logging.py b/ca_core/db_logging.py new file mode 100644 index 0000000..19fe6ef --- /dev/null +++ b/ca_core/db_logging.py @@ -0,0 +1,9 @@ +# db_logging.py + +def log_change(cursor, message: str): + """Insert a log entry into the log table.""" + cursor.execute( + "INSERT INTO log (entry) VALUES (%s)", + (message,) + ) + diff --git a/ca_core/entity.py b/ca_core/entity.py index 070a7d1..89fbdf8 100644 --- a/ca_core/entity.py +++ b/ca_core/entity.py @@ -1,50 +1,49 @@ -import random -import string +from db_logging import log_change -# ------------------------ -# Helper for ownership checks -# ------------------------ -def _verify_ownership(cursor, entity_id, requesting_creator_id): - cursor.execute( - "SELECT id, creator, type, status FROM entity WHERE id=%s", (entity_id,) - ) + +def ensure_entity_active(cursor, entity_id): + """ + Ensure an entity exists and is active. + Revoked entities are immutable. + """ + cursor.execute("SELECT status FROM entity WHERE id = %s", (entity_id,)) row = cursor.fetchone() - if not row or row["status"] != "active": - raise ValueError("Entity not found or inactive") - owner_id = row["creator"] - entity_type = row["type"] - entity_id_db = row["id"] - - if entity_type == "creator": - if requesting_creator_id != entity_id_db: - raise ValueError("Creator ID does not match entity owner") - else: - if requesting_creator_id != owner_id: - raise ValueError("Creator ID does not match entity owner") + if row is None: + raise ValueError("Entity does not exist") + if row["status"] != "active": + raise ValueError("Entity is not active") -# ------------------------ -# Insertions -# ------------------------ def insert_creator(cursor, name, public_key): + """ + Creators are persons with property 'creator' in the property table. + """ cursor.execute( """ - INSERT INTO entity (name, type, public_key, creator, status) - VALUES (%s, 'creator', %s, NULL, 'active') + INSERT INTO entity (name, type, public_key, status) + VALUES (%s, 'person', %s, 'active') RETURNING id """, - (name, public_key) + (name, public_key), ) - return cursor.fetchone()["id"] + creator_id = cursor.fetchone()["id"] + + # Mark as creator via property table (schema: property(id, property_name)) + cursor.execute( + """ + INSERT INTO property (id, property_name) + VALUES (%s, %s) + ON CONFLICT (id, property_name) DO NOTHING + """, + (creator_id, "creator"), + ) + + log_change(cursor, f"Created creator entity {creator_id} with name {name}") + return creator_id def enroll_person(cursor, name, public_key, creator_id): - cursor.execute( - "SELECT type, status FROM entity WHERE id=%s", (creator_id,) - ) - row = cursor.fetchone() - if not row or row["type"] != "creator" or row["status"] != "active": - raise ValueError("Provided creator_id does not correspond to a valid active creator") + ensure_entity_active(cursor, creator_id) cursor.execute( """ @@ -52,18 +51,16 @@ def enroll_person(cursor, name, public_key, creator_id): VALUES (%s, 'person', %s, %s, 'active') RETURNING id """, - (name, public_key, creator_id) + (name, public_key, creator_id), ) - return cursor.fetchone()["id"] + person_id = cursor.fetchone()["id"] + + log_change(cursor, f"Enrolled person {person_id} under creator {creator_id}") + return person_id def create_group(cursor, name, public_key, creator_id): - cursor.execute( - "SELECT type, status FROM entity WHERE id=%s", (creator_id,) - ) - row = cursor.fetchone() - if not row or row["type"] != "creator" or row["status"] != "active": - raise ValueError("Provided creator_id does not correspond to a valid active creator") + ensure_entity_active(cursor, creator_id) cursor.execute( """ @@ -71,99 +68,67 @@ def create_group(cursor, name, public_key, creator_id): VALUES (%s, 'group', %s, %s, 'active') RETURNING id """, - (name, public_key, creator_id) + (name, public_key, creator_id), ) - return cursor.fetchone()["id"] + group_id = cursor.fetchone()["id"] + + log_change(cursor, f"Created group {group_id} under creator {creator_id}") + return group_id -def create_alias(cursor, person_id): - cursor.execute( - "SELECT id, type, public_key, status FROM entity WHERE id=%s", (person_id,) - ) - row = cursor.fetchone() - if not row or row["status"] != "active": - raise ValueError("Person not found or inactive") - if row["type"] != "person": - raise ValueError("Only persons can create aliases") - - random_name = "".join(random.choices(string.ascii_letters + string.digits, k=8)) +def create_alias(cursor, target_entity_id): + ensure_entity_active(cursor, target_entity_id) cursor.execute( """ - INSERT INTO entity (name, type, public_key, creator, status) - VALUES (%s, 'person', %s, %s, 'active') + INSERT INTO entity (name, type, creator, status) + VALUES (%s, 'alias', %s, 'active') RETURNING id """, - (random_name, row["public_key"], person_id) + (f"alias_for_{target_entity_id}", target_entity_id), ) - return cursor.fetchone()["id"] + alias_id = cursor.fetchone()["id"] + + log_change(cursor, f"Created alias {alias_id} for entity {target_entity_id}") + return alias_id -# ------------------------ -# Soft-delete / revocation -# ------------------------ -def revoke_entity(cursor, entity_id, requesting_creator_id): - _verify_ownership(cursor, entity_id, requesting_creator_id) - cursor.execute( - "UPDATE entity SET status=%s WHERE id=%s", ("revoked", entity_id) - ) - - -# ------------------------ -# Getters / Setters -# ------------------------ def get_entity(cursor, entity_id): + cursor.execute("SELECT * FROM entity WHERE id = %s", (entity_id,)) + return cursor.fetchone() + + +def set_entity_status(cursor, entity_id, status, changed_by): + """ + Only active entities can change status. Once revoked, immutable. + """ + ensure_entity_active(cursor, entity_id) + + cursor.execute("UPDATE entity SET status = %s WHERE id = %s", (status, entity_id)) + log_change(cursor, f"Set status of entity {entity_id} to {status} by {changed_by}") + + +def set_symmetrical_key(cursor, entity_id, key_value, changed_by): + ensure_entity_active(cursor, entity_id) + cursor.execute( - "SELECT * FROM entity WHERE id=%s AND status='active'", (entity_id,) + "UPDATE entity SET symmetrical_key = %s WHERE id = %s", + (key_value, entity_id), ) + log_change(cursor, f"Set symmetrical_key for entity {entity_id} by {changed_by}") + + +def get_symmetrical_key(cursor, entity_id): + cursor.execute("SELECT symmetrical_key FROM entity WHERE id = %s", (entity_id,)) row = cursor.fetchone() - if not row: - raise ValueError("Entity not found or inactive") - return row + return row["symmetrical_key"] if row else None -def get_entity_id(cursor, name): +def set_entity_keys(cursor, entity_id, public_key, changed_by): + ensure_entity_active(cursor, entity_id) + cursor.execute( - "SELECT id FROM entity WHERE name=%s AND status='active'", (name,) + "UPDATE entity SET public_key = %s WHERE id = %s", + (public_key, entity_id), ) - row = cursor.fetchone() - if not row: - raise ValueError("Entity not found or inactive") - return row["id"] - - -def get_entity_public_key(cursor, entity_id): - cursor.execute( - "SELECT public_key FROM entity WHERE id=%s AND status='active'", (entity_id,) - ) - row = cursor.fetchone() - if not row: - raise ValueError("Entity not found or inactive") - return row["public_key"] - - -def get_entity_name(cursor, entity_id): - cursor.execute( - "SELECT name FROM entity WHERE id=%s AND status='active'", (entity_id,) - ) - row = cursor.fetchone() - if not row: - raise ValueError("Entity not found or inactive") - return row["name"] - - -def set_entity_name(cursor, entity_id, new_name, requesting_creator_id): - _verify_ownership(cursor, entity_id, requesting_creator_id) - cursor.execute("UPDATE entity SET name=%s WHERE id=%s", (new_name, entity_id)) - - -def set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id): - _verify_ownership(cursor, entity_id, requesting_creator_id) - cursor.execute( - "UPDATE entity SET public_key=%s WHERE id=%s", (public_key, entity_id) - ) - - -def set_entity_keys(cursor, entity_id, public_key, requesting_creator_id): - set_entity_public_key(cursor, entity_id, public_key, requesting_creator_id) - + log_change(cursor, f"Updated public key for entity {entity_id} by {changed_by}") diff --git a/ca_core/group_member.py b/ca_core/group_member.py index 4ab3142..c00cb63 100644 --- a/ca_core/group_member.py +++ b/ca_core/group_member.py @@ -1,42 +1,32 @@ -# ca_core/group_member.py +from db_logging import log_change +from entity import ensure_entity_active -def add_group_member(cursor, group_id: int, member_id: int, role: str): - # Verify group exists and is active - cursor.execute("SELECT type, status FROM entity WHERE id=%s", (group_id,)) - row = cursor.fetchone() - if not row or row["status"] != "active" or row["type"] != "group": - raise ValueError("Invalid or inactive group") - - # Verify member exists and is active - cursor.execute("SELECT status FROM entity WHERE id=%s", (member_id,)) - row = cursor.fetchone() - if not row or row["status"] != "active": - raise ValueError("Invalid or inactive member") + +def add_group_member(cursor, group_id, member_id, role): + ensure_entity_active(cursor, group_id) + ensure_entity_active(cursor, member_id) cursor.execute( - "INSERT INTO group_member (group_id, member_id, role) VALUES (%s, %s, %s)", + """ + INSERT INTO group_member (group_id, member_id, role) + VALUES (%s, %s, %s) + """, (group_id, member_id, role) ) - -def remove_group_member(cursor, group_id: int, member_id: int): - cursor.execute( - "DELETE FROM group_member WHERE group_id=%s AND member_id=%s", - (group_id, member_id) + log_change( + cursor, + f"Added member {member_id} to group {group_id} as {role}" ) -def get_groups_for_member(cursor, member_id: int): +def get_members_of_group(cursor, group_id): cursor.execute( - "SELECT group_id, role FROM group_member WHERE member_id=%s", - (member_id,) - ) - return cursor.fetchall() - - -def get_members_of_group(cursor, group_id: int): - cursor.execute( - "SELECT member_id, role FROM group_member WHERE group_id=%s", + """ + SELECT member_id, role + FROM group_member + WHERE group_id = %s + """, (group_id,) ) return cursor.fetchall() diff --git a/ca_core/metadata.py b/ca_core/metadata.py index 0413485..ce1cb20 100644 --- a/ca_core/metadata.py +++ b/ca_core/metadata.py @@ -1,40 +1,56 @@ -# ca_core/metadata.py +from db_logging import log_change + + +def set_name(cursor, name): + cursor.execute("DELETE FROM metadata") + cursor.execute( + "INSERT INTO metadata (name) VALUES (%s)", + (name,) + ) + log_change(cursor, f"Updated metadata name to {name}") + def get_name(cursor): cursor.execute("SELECT name FROM metadata LIMIT 1") row = cursor.fetchone() - return row['name'] if row else None + return row["name"] if row else None -def set_name(cursor, value): - cursor.execute("UPDATE metadata SET name=%s", (value,)) +def set_comment(cursor, comment): + cursor.execute("DELETE FROM metadata") + cursor.execute( + "INSERT INTO metadata (comment) VALUES (%s)", + (comment,) + ) + log_change(cursor, f"Updated metadata comment to {comment}") def get_comment(cursor): cursor.execute("SELECT comment FROM metadata LIMIT 1") row = cursor.fetchone() - return row['comment'] if row else None + return row["comment"] if row else None -def set_comment(cursor, value): - cursor.execute("UPDATE metadata SET comment=%s", (value,)) +def set_keys(cursor, public_key, private_key): + cursor.execute("DELETE FROM metadata") + cursor.execute( + """ + INSERT INTO metadata (public_key, private_key) + VALUES (%s, %s) + """, + (public_key, private_key) + ) + log_change(cursor, "Updated metadata keys") def get_public_key(cursor): cursor.execute("SELECT public_key FROM metadata LIMIT 1") row = cursor.fetchone() - return row['public_key'] if row else None + return row["public_key"] if row else None def get_private_key(cursor): cursor.execute("SELECT private_key FROM metadata LIMIT 1") row = cursor.fetchone() - return row['private_key'] if row else None - - -def set_keys(cursor, public_key, private_key): - cursor.execute( - "UPDATE metadata SET public_key=%s, private_key=%s", - (public_key, private_key) - ) + return row["private_key"] if row else None diff --git a/ca_core/property.py b/ca_core/property.py index eaacf42..9b91627 100644 --- a/ca_core/property.py +++ b/ca_core/property.py @@ -1,29 +1,42 @@ -# ca_core/property.py +from db_logging import log_change +from entity import ensure_entity_active + + +def set_property(cursor, entity_id, property_name): + """ + Revoked entities are immutable: cannot add properties. + Schema: property(id, property_name) + """ + ensure_entity_active(cursor, entity_id) -def set_property(cursor, entity_id: int, property_name: str): cursor.execute( """ INSERT INTO property (id, property_name) VALUES (%s, %s) ON CONFLICT (id, property_name) DO NOTHING """, - (entity_id, property_name) + (entity_id, property_name), ) + log_change(cursor, f"Set property '{property_name}' for entity {entity_id}") -def delete_property(cursor, entity_id: int, property_name: str): +def get_properties(cursor, entity_id): cursor.execute( - "DELETE FROM property WHERE id=%s AND property_name=%s", - (entity_id, property_name) + "SELECT property_name FROM property WHERE id = %s", + (entity_id,), ) - if cursor.rowcount == 0: - raise ValueError("Property not found") + rows = cursor.fetchall() + return [r["property_name"] for r in rows] -def get_properties(cursor, entity_id: int): +def delete_property(cursor, entity_id, property_name): + """ + Revoked entities are immutable: cannot delete properties. + """ + ensure_entity_active(cursor, entity_id) + cursor.execute( - "SELECT property_name FROM property WHERE id=%s", - (entity_id,) + "DELETE FROM property WHERE id = %s AND property_name = %s", + (entity_id, property_name), ) - return [row['property_name'] for row in cursor.fetchall()] - + log_change(cursor, f"Deleted property '{property_name}' for entity {entity_id}") diff --git a/create_tables.sql b/create_tables.sql index 7d2524b..68ace05 100644 --- a/create_tables.sql +++ b/create_tables.sql @@ -1,62 +1,69 @@ -- ------------------------ --- Metadata table (singleton) +-- Metadata -- ------------------------ DROP TABLE IF EXISTS metadata; -CREATE TABLE metadata ( +CREATE TABLE metadata( name VARCHAR(50), comment VARCHAR(200), private_key VARCHAR(500), public_key VARCHAR(500) ); -INSERT INTO metadata DEFAULT VALUES; - -- ------------------------ --- Entity table +-- Entity -- ------------------------ DROP TABLE IF EXISTS entity CASCADE; -CREATE TABLE entity ( +CREATE TABLE entity( id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), creator INT REFERENCES entity(id), name VARCHAR(100) NOT NULL, - type VARCHAR(10) NOT NULL DEFAULT 'person', -- 'creator', 'person', 'group', 'device' - geo_offset BIGINT, + type VARCHAR(20) NOT NULL, -- person, group, device + symmetrical_key VARCHAR(100), public_key VARCHAR(300) NOT NULL, - expiration DATE, - status VARCHAR(10) NOT NULL DEFAULT 'active' + status VARCHAR(20) NOT NULL DEFAULT 'active', + expiration DATE ); --- Indexes CREATE INDEX idx_entity_name ON entity(name); -CREATE INDEX idx_entity_expiration ON entity(expiration); - -ALTER TABLE entity ADD CONSTRAINT entity_name_unique UNIQUE (name); -- ------------------------ --- Group Member table +-- Group Member -- ------------------------ DROP TABLE IF EXISTS group_member; -CREATE TABLE group_member ( +CREATE TABLE group_member( group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, member_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, role VARCHAR(10), PRIMARY KEY (group_id, member_id) ); -CREATE INDEX idx_group_member_member_group ON group_member(member_id, group_id); +CREATE INDEX idx_group_member ON group_member(member_id, group_id); -- ------------------------ --- Property table +-- Property -- ------------------------ DROP TABLE IF EXISTS property; -CREATE TABLE property ( +CREATE TABLE property( id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, property_name VARCHAR(100), PRIMARY KEY (id, property_name) ); +-- ------------------------ +-- Log Table +-- ------------------------ +DROP TABLE IF EXISTS log; + +CREATE TABLE log( + id SERIAL PRIMARY KEY, + ts TIMESTAMPTZ NOT NULL DEFAULT now(), + entry TEXT NOT NULL +); + +CREATE INDEX idx_log_ts ON log(ts); + diff --git a/tests/__pycache__/test_entity.cpython-313.pyc b/tests/__pycache__/test_entity.cpython-313.pyc index fe1813996fe9368b0d0b71c05dd8817309c41980..461cf8b92c947f0fc58d398608a04d8d99fc956d 100644 GIT binary patch literal 5164 zcmeHLO>7&-6`tiT$st8jvPq?so1j4+d(ZUiR)N#xt7~l0yflYO*5u; z)!7wn3O&d`QADTOc>rYVLhAyDO41EfqzJ zTsi>XyqS6P=I4Fy&0B7S!$Ab)>A7O=Zy2F(S;H+phuQc$Ft?D5WZ@(tb}bMA+Mbi1 zUx-9Z*nJX_#>7@M=Is;gndy+Ls$AFVlf|FZAw>LRLb{2m3)%av;xjRj#Pzq?YO z#~yQbQb+1!iG*ZtGa}(QYDKcI8Oi=Jl-^U;;c|VV&5`=7ki_4y#&A-g(&>zKf%?^q zq8db_9yOyn$Kv8_68mj@0^}ANLFvj-=wn3|+zlp}gtL%6X45{Wp$j8IyTfO^2#pA` z2P*VBlEv3iQW~D=l!rbX8aR{Cbc+-c@1;LFmB<^}#7F7Dp>*Q-$BEoTVsJuU*#EX-lK{yk@Bv3|)gxpE^xUgHVwemrQnji3B~Ex?oIdscBOq zsi}*(l%<)L$*BU`a#o>psz5_o%~J9i6Nr%|5ttcM?7&}I_RaOoU7A0#B()Wf1K9w1q%z_b$A)O**U)ndjUk3H;*kX+j!Cko z8M&85ywDaa+CEtVOZ&OKF|#!Z$bO(f83V1813*J^kc6{!$qh2^R32opxx3|=Cr$Kx7)1m!o)4iIpC$`yq=9jItH zY{hEaKdU8b`jF?a0BiYO$t+5W!!!PgaXmAs9R)nsft{7}8PzgKA&IGf$}FhHR2C3! z=(?s_)His1_~)mFsKh!5I3%JH!v@6@In`2tr6OQvQGhCMnhfp-a zdq2OJFLe$qcMe=nE$tqh6aU~R^)S22Qp_IQQh8vwD?+`sZ0CI3IXA6Wp1J*;ELs$X zlOkz>3Dm3R4e**c@Px&f%IgVRT80c7mvsAv4%E7#Dv@TUU@wqa^c2@!^UwRQh3CVc z*RMkZ&*N*6`N$fMZ*vj-F!w*Zh%6!pVDh4eABAFK|Qb5Q)|QSXe*n`V%f_( zWbv5e>knh^oKy#}Ua2<+~QqYD)}YD9)s_Gr}5F(SC{o$djN8u4SqBa3Xi z@Ble`OQ2Ea(!A8x>PDsc&OjNOahjjzNyO0h%B zu|q4d?zsSSLkJ~HueB`%R&lq}{F8d8*e_w5wLu-WC7cNy%7n#w!d_JJ4v?gcVR8g`8nE>+93O@D z7Vz%o;H}a~+g^#;D!ERkjlH(pKsDA}inTwEwJ*f(#~0%d4nH)$o>_^Vn+xm&%{LdC zR&h@mn#r4bF}H>f{U4fe;SkdQ>(K0Rq1i)y?-OHss*8pkWM)}&Cd>mO;F33AhGobe zSNQObsKi|C6`z*mJ!*0bF%pH8cuD4$cY~~Si<+CSPqu(_%rD6l7`Rxt>xcLG-?D= zNHip#XjBBg(am7FLtx$vpymBk1?J6!p*&ulXxJ%3aKO$C0r74*+2aM0XFZ z2X<1kNKVb<73fO_wuaUyWUR^-DZ)TSe#!{@%&iEuKZE9$aGK!AWm=pBV5>IT`$`~h zGwT>z>32Y0#`V~vBP)%kAuGU<5^jBrTZ_H-`tSBHoO{^v2y%d5KHDi=X@py-X4H5ZvJ?HFm3@qzy+U#t4fd%j07d}q> z-tHH>ecrAY4L-LeX~-B0CIn9SlPI4XCoDrEjX>DNoX_#g z@J(e1WQ{bS8WWnrB&o!y&(0Y6gKgvRM{GL77{v@E0~yFy`H8AeN%0Z;s*jORL6{}c z<|ja&i-I6LLG@2i@Xsjz1V#QDYPcT%Y`7FUupBy23bikX+7}Klhk9oN>qxwcui&fo xSL&||H+rrYZXR1jt>2>X(?EFE_d*hdH~$&x5qj^mmzv*NZhq@~1Py)#{{q>i^ltzF literal 5177 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 literal 6983 zcmds6U2s#!6<+;cNmsVcBeq87y4nh|dwLy=vZwRKul9r(^l6(k}wxtQdx=YC}TjAx9GNV)WUB%%33n2(z+_#!lIji z{I>87M`An^n-X7#yWqyzYr;e@}Z%i}6{kSP=3tn3)q1slFur6kAXoMfnLFIrx~ zX1D^WH^qY`r{OnRvF`#LjTSxTZLUU=VqBOKr^ZEG;ex2$+BKm!N;%YuC&gU~gN1z| zxP!p}9}IL=Se-hL@Nd(Hv+0ua-MpVgx0mzjt2xXWh7`7qZd9S=cM-?5G!xHGC&Bw> zGZ`^16MNVBaG*a#tdv3Q@f=(oOsoJ)i7<&&To#~8EWjrY@wyn#$s$N)r>0Uez}FH_ zOA_^;9BFBr%ub1IIZ4EA(^pe%GI(1_p@L%Lk?9%Yf}1xu9Zbh0iR|9&j*k?&X?&Be z0lA63sB3=z+%L|}UH*7zzWee1&Xu|ovzDJTPiq_A_r2?TzwO<&kCXS)3+eok;L?%c zowmodUC{4Xw<9O}j^noDo$A}w>xgY)ze23jv))|7c2Kqh*fo^R+-9is6Q*ecrY?;P z)c-4yVFW67sn4V$WhG*G{X$2<`|;qbD~(DUu_cN8=I{l-1yg@a7`O}R@mTySam8|S zR^9al2%wUfo=~?PH&Q{HvT?+_)t0^rLed@}H_=mueaCs*nP-}pnC4Yx*VDZ(&sr(x z39}0~fk;U>G!}gldcNJFVM(U2->j?%zXWRVZXm_hGqM=NUD<0H@=}Eb=$-9!6r>yF zYp7^94NS^m4Z{lVqN2cRY&A$|rG*}*5yo{cgh&%_)>EF0sYpkKu(zcS3vV4Xb!yr( z<{~XjVJ5w{r4}1+FVbQu(()qfaqx_>9_y%)VpK{jwz6b+Kkw96!=Z1f<7M7eQAdR& zWGg!c&3Y=Eu?^bl(2!j+w;{<~hq@sd%1-cZ8*J);xgXLjH{wvnk+H)mS4f10U(+=y ziBKhpiv@`!%)!C`jC}(6*)U;(G+W5xc5tn^afq7j#D0;+p8#Q1hU)!@V?YVY?Zl(8 z)gVnAUE-UmxLDvPls2)e?wr`QwzJ^goy4wYKcp&^0e344O3ISOn8JgwN=ww{^@thj zMFed+R*e^$@|sv8p~`9DlGGPCo5n-RhuVe}A0oa!W$ z=~T_8Rfjs6L`+k}WvoJy=>4YwiWb$4d1=fLs7TktLRJoxcS{iRvw7fgMgIk3ze zm`i*#`QhXu^Kf{T39nV=*6zvIb}ZL+EOz~_`;+bmlPk3sXB~e!(z()b;y20p9rxVt zWfqdNuEz~0sGs)S_S{LWGKY=&6Xx(gp4(94YZ7%qpB}I3I%qBN*M{Hw{Wto2`H%R$ z7xA9ue$Vik5yN}IMrdU#UjaVLQRHt5Ix2j2Z>6Ki-&kSS;0d6-3|e`H`W)U#>7b?H zGrmUNrHpbE~6>CpVh6XxXREQ)@fE7{0{|6WuYr~q5FvK1Eia#ZRA&L7?n{YAJ88i%n->(ji;{&4R>W_E1OGw06coZzhcDgLx+)1)0Kcznt&+3 z7iNRUEA+Z$FPeY{tO6!GbWC<=(f+&<))l~=*TelWQl-OG3IE41O~w7Liumh*F5wX1?&%Lgn`3`sj4;0oewon*#2+M{lguU9J#tAD<0B9vE4vABB+1 zROOjn%gnBO(O(aLFg!Q2%Dn#6vm@`>v*g+Hs~b-|t!u0|&-#~H9pl}PysK>F?-o>b zDDP?gqo?(s8`R1FVekH++w!T~6>M^p#LNFPWDf5uV5;aF;RRx4Ikq8o{ZAqDxQ@)@ z8Zv{W$PDVp>?lKK$9BjZf;)sMJn>~p;NiPCT3&%>8Qgp&a8rDOj<#4lE>0UcHu=ui zVQ!njQ9ALr23{GsuCFp@Hf6s>V0=;F;W4mo0UrE4gb@V#!YK&EB?wd5L@rJ1tRT?T zLhGp(gozZE&rh0d2c9z~ z+wSL#+xF`7X2y2vdA;4noBmcqsj86bqHCLqr80uxuO&<_lalGl!zF}2aimk@n1+2~ zk!BbI)znBMKUGxN){tElC3iK4^>&KKp252@skgHZ^HXj4u>y;)~I9hFwC9X}rmNbLN|K z&zzrc?w2h;9SCj*0$u{^qmjjlPo;%m%<(H(X#Fmd6XS#hV&ovnNpX^MzeBPK{=`$a z@HZ7g{G=c^3AnAYoN&i$`!c}CWSC?HzTl_}38{*gjGPk#@Q0H7Mo}0RTEP{UDiSg* zakzdbVydW1!JdwE4ry&EfsYjC2fisC1yEUY1-ho#SYCsuABLSY5
b|RGu@7Jy z{E3E8U5<*eo#jR0P0IY^Ud3lc`J6YJ_^l3JYI|Gf0>WMOkD%*vBu4(b^@!a%u&`z@Re28u<5OM=%P8@e;rs zd0ZFge~L-hZ8r&+S?OXe9OD@yvVHX?&T+*Jz@W0mR#f z@mLE2N?|vE-_!0ph%8P5+<*7hJ9-5EuIDq)2o{Zt-}E&(vVp3eB+wJdhCp(W@UW1o z48YbLl67x5_p;DlSH^O`RxW3tzq5f!-Bia^Qm-3=hUt{Gg3S9 zBmA5{)wW8{KELey`&z$H)@VAB}uiUVGwT`UB4O2BIw7hDX zY+@7+IwPNF=gg2g^FP>AlUx}MTwpC5)}|#1kg!0vVNK$Ykl?I54vV^xMiXa}SU9$O zXS_fjDz;J!5i14SB3P-?(w8ccR)T$6{tWV^M3+`V;%VQ)hQ9ZlvB$Av6CzX|dLNGO zJ@?#?bI$Lad&XN04ITnbz4Nzp)J@1s99Yd+>TER%gxn`Gk%b|`ioXIAU~C(*T@;xp z6?t}M9~0Zin4=ryu_b)P-%47KCyU3tgfQ2bkZ7!wE%p=pYk4vm^T0_i>!fa5QKobg zFZ0TF<_nTGB0HLi>>MMB=GtAl#UhXS#{#nJ>=m%XK0-iNs+Dac-6f_(ri3XWLVv<> zJR)%W=c+NyT`5&b<(S58DK%BOzH~LR1^a^;#|WB^*<^uh>ssgYPxYrGt>#yQ2Ptnl#$M5^`HE3YO3TA z^ICSGsQA`1)2&~cOe6{GrezL60}5Z_3bXmiNt6 zW&*EzzAiXyX9PIQWl6$WsM3y>XxhPmp)6W=mGFjpU1fjksycwQz>yV31=-=i)14-u z4w}~4>)B;Mn>Vg!(*w17T4w_=Nrc8{T@&M3bw>LwK-0@|*OW;$W#rh6*_R%vqV* zl!Z=o)|1M!^jE5(DOa@{Wy$XPl7;b1I#nZBb#A0QnmsJXhGV@+sdr>Fo(vz2NKsiz z!FBo*BbUmGaS>Py;XZq4FfPXuNog>i94XIBeX;)N=x`FQHar@WBVO*x>o-!l{1k+F zE}PX-26y(Ik4G=XxE&eXnJO?n$G9B~u5&t(P8kYx;h+NJF73LODi|84lbVs5&Sf=_ z2Q#IOhz%q%Iu~&n-ba%7k&fxyjMh=mHP(^8n(iIn7=T0VSZ$8Zm=RQUU+!AA=#4}5y-_7}1h@DQ$W8JB^hc{U%r$%yu#DlK^RLk$ zYXOO>q%0JPI4?tw2z1-@JRyy-u5#X!9y<+4$w*+jiuv(Ux8GJ`IcCP46kf z@Z^ZiREr&gJ~Q1=;Yn&aiMQ6*!kX`Z)jAXlI~A#Wd3+8`1pgB{_esSYST;FKlv*1i zybfArVY|oyiSm1Bts|BD(rvEQd(*zwYgf6y2>|Yj=JRpL_YnuDR_5Up{HWk7Pz($& zq#unLvELWe45w4p+)le5Fd&I3xKzzfD3%!i)tii5N4N_V(J`cNl7G?0bsAcxp@)aR z3x6AaG`LFJH>hu&2A62?@4@xZ(dE$5g|5e^7f-K*x}K!h=x4Qv`u}vwtPf2ll^)rM zgSCT2xEC#3;+cM7ohBO%ZLxQ{Bv|8;k|b~6qgP7uXLdNIWtp}-On!Ie+bfU$uu4x= zIp)}M=-9%<sls!!h(Ewarj@6B53y~fL1_J z9H%JUgK)SEq&7~6p0upeo+?0eE{8gweDGuNhu})+?9-uDI#4xzd^vRd$>ATvKZI98 zzj->gMlbGym*M{zUfwZ7i?aPaV@}-0#CyS^yc)_qVap! z7|39Kre6oLDGGw{oCKZ|&%a6VIq|=8l7^rbz6~rm6l5U3;b3 diff --git a/tests/__pycache__/test_property.cpython-313.pyc b/tests/__pycache__/test_property.cpython-313.pyc index ebab9f6949569b120df9f5b929b0a72afa9f64bf..5662eae868a0e6713540573bdbe4b4fd7a0d7893 100644 GIT binary patch literal 5353 zcmeHLTWk~A89w9j<=7cJ4mi6e+=jdDE(s8_3+2}0WCaTt)LkXCLZniqhL@5^E0KpjRDEc-U8&Ex7&$u73Tb)Smx?93Lh4ihGvkRJ*U7GE zEA^o#%71SELF%y>DT=$OPx*?D(8}@6>4^%fOGaj5wuwn`k)=BX`jNV%|nv z5=Zy}x8&+T&T$bB^!d8JS8~H$`>Y$s?6!$t@&OG<7-);+Cqh0DZ=vm%VM~{)dwIL4 zWldGrjL%;Kd#kS34FlHo{?=N_6`*!WJ8N_Xpa%cqt}qF!giyE3x%PfkSh;j?00&tP zE7WO!U8mZtvu2@mHAhnH0YTurO1obSCFJlM`KY8+u3ik9?#Rq*vV)yAkWI3ed2i2z6sw4}2ClSMIMS zxxP#u&2XtWPd0-9^<)b=poVRaYynb#KtWT+n|B*ZVX%H@ z9_wQ1=H@%HCU=6aaf~0G63qEc$Ng=-N`!|SEr*%gxJYaM>8F~pcKiH1}SB7rGqZH0Dx2r3!_lA{7Ua+GOVNv<@+GHZ2CRUr66 zPz7CQH7m}Ir_TxYRFyViom8UhR=#&3X(peOB{EA5Ge_Qdqzheu|P%zp2aO^fa4MgvbG9hFFI zAre~>!sBPg&P=FN+h-HYysJGh>i-fWe_I7_UBFu>bH5(?<!3LR=>0#M;U>|V5rIAQ8p=0}2rGE6i8n-qmAC%aM1=O52B)jeCbr9UZdx|OjBar_vc%R^K z1&a%~3eV}Iv-vVUxhAA5I69A`ldn(hDdX7spq_gzHXSeH!`~Ry(f>76!)e=oEh zUd?_U?LcoJ=W{s+@329ah_=ISMU=XDg5Qn?xwSd+$mXce`rQu3p|c)K34hVP?#tK?NJl>5OVX9uK^mDHN4HLUdA#1LPez4fudy$(%4S zlJ5X9*yC~~SMRYvUR{J^Vkl$Cn(l-T_D(pq4(h*7SYk$h59Hqn1Fa2!S3l_}n(U)1}|-D+{MB*12Hfr4K|v;Xhu2nAI@Ych{dz?n);3KLb6+f0_s+kN7@47U=c+ zX5iCo0s;0pC%CsfRzG|;4=8&Ev z?588Li;)B)os1+IInT&@Kxn&cUB8-@Gp0$@tMHqPA}*=dE{+-ZZ)4{XtL2wD}$R}V9vAkg_n^~(35zvH{?lt6%#y3&Njew z$J;0_YO2Xxhw?_r0Oy9^vI^=|!hDOc9LmmPP_@p$JDeflyVRf6bBfG3soSDn`ymS| zOk%semBLv1w4bAxd0jFCFH~zwtG7wB0k{^NufD{=jkmo$dagR~!V-)x+iati6 zFT5z&HWB?<3cO=p*ijK;^FnO;=)7=v#J`02+xQl~-FmBag1gf_QToNnGTQkW3W9>C i{@{rBg_q~{eJPyfxFk1urm|)K{FeP+A**X0$UgxJQVL=K literal 5578 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 diff --git a/tests/test_entity.py b/tests/test_entity.py index 7c20c8e..40b4fea 100644 --- a/tests/test_entity.py +++ b/tests/test_entity.py @@ -1,15 +1,20 @@ import unittest -from pathlib import Path import sys +from pathlib import Path import psycopg -# Add code folder to path code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import entity # the rewritten entity.py module + +import entity DBNAME = "ca" +def get_last_log(cursor): + cursor.execute("SELECT entry FROM log ORDER BY id DESC LIMIT 1") + row = cursor.fetchone() + return row["entry"] if row else "" + class TestEntityFunctions(unittest.TestCase): @@ -18,56 +23,49 @@ class TestEntityFunctions(unittest.TestCase): cls.conn = psycopg.connect(f"dbname={DBNAME}") cls.cur = cls.conn.cursor(row_factory=psycopg.rows.dict_row) - # Ensure table exists - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS entity ( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), - creator INT REFERENCES entity(id), - name VARCHAR(100) NOT NULL, - type VARCHAR(10) NOT NULL DEFAULT 'person', - geo_offset BIGINT, - public_key VARCHAR(300) NOT NULL, - expiration DATE, - status VARCHAR(10) NOT NULL DEFAULT 'active' - ) - """) - cls.conn.commit() + @classmethod + def tearDownClass(cls): + cls.cur.close() + cls.conn.close() def setUp(self): self.conn.rollback() - self.conn.autocommit = False def tearDown(self): self.conn.rollback() - # --- Insert and read --- def test_insert_creator_and_get(self): creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1") row = entity.get_entity(self.cur, creator_id) self.assertEqual(row["name"], "Creator1") - self.assertEqual(row["type"], "creator") + + log_entry = get_last_log(self.cur).lower() + self.assertIn("creator entity", log_entry) + self.assertIn(str(creator_id), log_entry) def test_enroll_person(self): creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2") person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id) - self.assertEqual(entity.get_entity_name(self.cur, person_id), "Person1") - self.assertEqual(entity.get_entity(self.cur, person_id)["type"], "person") + + log_entry = get_last_log(self.cur).lower() + self.assertIn("enrolled person", log_entry) + self.assertIn(str(person_id), log_entry) def test_create_group(self): creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id) - self.assertEqual(entity.get_entity_name(self.cur, group_id), "Group1") - self.assertEqual(entity.get_entity(self.cur, group_id)["type"], "group") - # --- Revocation --- - def test_revoke_entity(self): - creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4") - entity.revoke_entity(self.cur, creator_id, creator_id) - with self.assertRaises(ValueError): - entity.get_entity(self.cur, creator_id) + log_entry = get_last_log(self.cur).lower() + self.assertIn("created group", log_entry) + self.assertIn(str(group_id), log_entry) + def test_set_and_get_symmetrical_key(self): + creator_id = entity.insert_creator(self.cur, "CreatorSym", "pubkey_sym") + entity.set_symmetrical_key(self.cur, creator_id, "symkey123", creator_id) -if __name__ == "__main__": - unittest.main() + row = entity.get_entity(self.cur, creator_id) + self.assertEqual(row["symmetrical_key"], "symkey123") + + log_entry = get_last_log(self.cur).lower() + self.assertIn("symmetrical_key", log_entry) diff --git a/tests/test_group.py b/tests/test_group.py index 2d637b4..1984922 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,6 +1,6 @@ import unittest -from pathlib import Path import sys +from pathlib import Path import psycopg code_path = Path(__file__).parent.parent / "ca_core" @@ -11,6 +11,12 @@ import group_member DBNAME = "ca" +def get_last_log(cursor): + cursor.execute("SELECT entry FROM log ORDER BY id DESC LIMIT 1") + row = cursor.fetchone() + return row["entry"] if row else "" + + class TestGroupFunctions(unittest.TestCase): @classmethod @@ -18,78 +24,66 @@ class TestGroupFunctions(unittest.TestCase): cls.conn = psycopg.connect(f"dbname={DBNAME}") cls.cur = cls.conn.cursor(row_factory=psycopg.rows.dict_row) - # Ensure tables exist - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS entity ( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), - creator INT REFERENCES entity(id), - name VARCHAR(100) NOT NULL, - type VARCHAR(10) NOT NULL DEFAULT 'person', - geo_offset BIGINT, - public_key VARCHAR(300) NOT NULL, - expiration DATE, - status VARCHAR(10) NOT NULL DEFAULT 'active' - ) - """) - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS group_member ( - group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - member_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - role VARCHAR(10), - PRIMARY KEY (group_id, member_id) - ) - """) - cls.conn.commit() + @classmethod + def tearDownClass(cls): + cls.cur.close() + cls.conn.close() def setUp(self): self.conn.rollback() - self.conn.autocommit = False def tearDown(self): self.conn.rollback() - # --- Group membership tests --- def test_add_and_get_members(self): creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1") - group_id = entity.create_group(self.cur, "GroupA", "pubkey_group", creator_id) person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id) - device_id = entity.insert_creator(self.cur, "Device1", "pubkey_device") - # Add members + group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id) + group_member.add_group_member(self.cur, group_id, person_id, "member") - group_member.add_group_member(self.cur, group_id, device_id, "device") + members = group_member.get_members_of_group(self.cur, group_id) - member_ids = [m["member_id"] for m in members] - self.assertIn(person_id, member_ids) - self.assertIn(device_id, member_ids) + + self.assertTrue( + any(m["member_id"] == person_id and m["role"] == "member" + for m in members) + ) + + log_entry = get_last_log(self.cur).lower() + self.assertIn("added member", log_entry) + self.assertIn(str(group_id), log_entry) def test_nested_groups(self): creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2") - parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_pg", creator_id) - child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_cg", creator_id) - # Add child group as member of parent - group_member.add_group_member(self.cur, parent_group, child_group, "subgroup") - members = group_member.get_members_of_group(self.cur, parent_group) - self.assertEqual(members[0]["member_id"], child_group) - self.assertEqual(members[0]["role"], "subgroup") + parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_parent", creator_id) + child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_child", creator_id) - def test_revoked_member_cannot_be_added(self): - creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") - group_id = entity.create_group(self.cur, "GroupB", "pubkey_groupB", creator_id) - person_id = entity.enroll_person(self.cur, "Person2", "pubkey_person2", creator_id) - entity.revoke_entity(self.cur, person_id, creator_id) - with self.assertRaises(ValueError): - group_member.add_group_member(self.cur, group_id, person_id, "member") + group_member.add_group_member(self.cur, parent_group, child_group, "subgroup") + + members = group_member.get_members_of_group(self.cur, parent_group) + + self.assertTrue( + any(m["member_id"] == child_group and m["role"] == "subgroup" + for m in members) + ) def test_revoked_group_cannot_accept_members(self): - creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4") - group_id = entity.create_group(self.cur, "GroupC", "pubkey_groupC", creator_id) - entity.revoke_entity(self.cur, group_id, creator_id) - person_id = entity.enroll_person(self.cur, "Person3", "pubkey_person3", creator_id) + creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") + group_id = entity.create_group(self.cur, "RevokedGroup", "pubkey_group", creator_id) + person_id = entity.enroll_person(self.cur, "Person2", "pubkey_person", creator_id) + + entity.set_entity_status(self.cur, group_id, "revoked", creator_id) + with self.assertRaises(ValueError): group_member.add_group_member(self.cur, group_id, person_id, "member") + def test_revoked_member_cannot_be_added(self): + creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4") + group_id = entity.create_group(self.cur, "ActiveGroup", "pubkey_group", creator_id) + person_id = entity.enroll_person(self.cur, "RevokedPerson", "pubkey_person", creator_id) -if __name__ == "__main__": - unittest.main() + entity.set_entity_status(self.cur, person_id, "revoked", creator_id) + + with self.assertRaises(ValueError): + group_member.add_group_member(self.cur, group_id, person_id, "member") diff --git a/tests/test_metadata.py b/tests/test_metadata.py old mode 100755 new mode 100644 index 2604121..d93cc8e --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -3,74 +3,58 @@ import sys from pathlib import Path import psycopg -# Add the code directory to Python path code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) -import metadata # your metadata.py module +import metadata DBNAME = "ca" +def get_last_log(cursor): + cursor.execute("SELECT entry FROM log ORDER BY id DESC LIMIT 1") + row = cursor.fetchone() + return row["entry"] if row else "" + + class TestMetadataFunctions(unittest.TestCase): @classmethod def setUpClass(cls): - # Connect to the database cls.conn = psycopg.connect(f"dbname={DBNAME}") cls.cur = cls.conn.cursor(row_factory=psycopg.rows.dict_row) - # Ensure table exists and has exactly one row - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS metadata ( - name VARCHAR(50), - comment VARCHAR(200), - private_key VARCHAR(500), - public_key VARCHAR(500) - ) - """) - cls.cur.execute("SELECT COUNT(*) AS cnt FROM metadata") - row = cls.cur.fetchone() - if row['cnt'] == 0: - cls.cur.execute("INSERT INTO metadata DEFAULT VALUES") - cls.conn.commit() - @classmethod def tearDownClass(cls): cls.cur.close() cls.conn.close() def setUp(self): - # Begin transaction for each test self.conn.rollback() - self.conn.autocommit = False def tearDown(self): - # Rollback after each test self.conn.rollback() - # --- Test name field --- def test_set_and_get_name(self): metadata.set_name(self.cur, "AppName") self.assertEqual(metadata.get_name(self.cur), "AppName") - # --- Test comment field --- + log_entry = get_last_log(self.cur).lower() + self.assertIn("metadata name", log_entry) + self.assertIn("appname", log_entry) + def test_set_and_get_comment(self): metadata.set_comment(self.cur, "Test comment") self.assertEqual(metadata.get_comment(self.cur), "Test comment") - # --- Test keys --- + log_entry = get_last_log(self.cur).lower() + self.assertIn("metadata comment", log_entry) + def test_set_and_get_keys(self): metadata.set_keys(self.cur, "pubkey123", "privkey456") + self.assertEqual(metadata.get_public_key(self.cur), "pubkey123") self.assertEqual(metadata.get_private_key(self.cur), "privkey456") - # --- Test keys overwrite --- - def test_keys_overwrite(self): - metadata.set_keys(self.cur, "pub1", "priv1") - metadata.set_keys(self.cur, "pub2", "priv2") - self.assertEqual(metadata.get_public_key(self.cur), "pub2") - self.assertEqual(metadata.get_private_key(self.cur), "priv2") - -if __name__ == "__main__": - unittest.main() + log_entry = get_last_log(self.cur).lower() + self.assertIn("metadata keys", log_entry) diff --git a/tests/test_property.py b/tests/test_property.py index 85c320d..87d3027 100644 --- a/tests/test_property.py +++ b/tests/test_property.py @@ -1,8 +1,9 @@ import unittest -from pathlib import Path import sys +from pathlib import Path import psycopg +# Add core directory to path code_path = Path(__file__).parent.parent / "ca_core" sys.path.insert(0, str(code_path)) @@ -11,6 +12,13 @@ import property DBNAME = "ca" + +def get_last_log(cursor): + cursor.execute("SELECT entry FROM log ORDER BY id DESC LIMIT 1") + row = cursor.fetchone() + return row["entry"] if row else "" + + class TestPropertyFunctions(unittest.TestCase): @classmethod @@ -18,68 +26,71 @@ class TestPropertyFunctions(unittest.TestCase): cls.conn = psycopg.connect(f"dbname={DBNAME}") cls.cur = cls.conn.cursor(row_factory=psycopg.rows.dict_row) - # Ensure entity table exists - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS entity ( - id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, - creation_ts TIMESTAMPTZ NOT NULL DEFAULT now(), - creator INT REFERENCES entity(id), - name VARCHAR(100) NOT NULL, - type VARCHAR(10) NOT NULL DEFAULT 'person', - geo_offset BIGINT, - public_key VARCHAR(300) NOT NULL, - expiration DATE, - status VARCHAR(10) NOT NULL DEFAULT 'active' - ) - """) - cls.cur.execute(""" - CREATE TABLE IF NOT EXISTS property ( - id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE, - property_name VARCHAR(100), - PRIMARY KEY (id, property_name) - ) - """) - cls.conn.commit() + @classmethod + def tearDownClass(cls): + cls.cur.close() + cls.conn.close() def setUp(self): self.conn.rollback() - self.conn.autocommit = False def tearDown(self): self.conn.rollback() + # ------------------------------------------------------------ + # Basic property set/get + # ------------------------------------------------------------ + def test_set_and_get_property(self): creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1") - person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id) - property.set_property(self.cur, person_id, "email") + person_id = entity.enroll_person( + self.cur, "Person1", "pubkey_person", creator_id + ) + + property.set_property(self.cur, person_id, "prop1") + props = property.get_properties(self.cur, person_id) - self.assertIn("email", props) + self.assertIn("prop1", props) + + log_entry = get_last_log(self.cur).lower() + self.assertIn("set property", log_entry) + self.assertIn("prop1", log_entry) + + # ------------------------------------------------------------ + # Delete property + # ------------------------------------------------------------ def test_delete_property(self): creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2") - person_id = entity.enroll_person(self.cur, "Person2", "pubkey_person2", creator_id) - property.set_property(self.cur, person_id, "phone") - property.delete_property(self.cur, person_id, "phone") + person_id = entity.enroll_person( + self.cur, "Person2", "pubkey_person", creator_id + ) + + property.set_property(self.cur, person_id, "prop2") + property.delete_property(self.cur, person_id, "prop2") + props = property.get_properties(self.cur, person_id) - self.assertNotIn("phone", props) + self.assertNotIn("prop2", props) + + log_entry = get_last_log(self.cur).lower() + self.assertIn("deleted property", log_entry) + self.assertIn("prop2", log_entry) + + # ------------------------------------------------------------ + # Immutability: revoked entity cannot mutate properties + # ------------------------------------------------------------ def test_revoked_entity_has_no_properties(self): creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3") - person_id = entity.enroll_person(self.cur, "Person3", "pubkey_person3", creator_id) - property.set_property(self.cur, person_id, "address") - entity.revoke_entity(self.cur, person_id, creator_id) - props = property.get_properties(self.cur, person_id) - # Optional: you can decide whether to return empty or raise; here we return all properties regardless of status - # If you want to ignore revoked entities: - cursor = self.cur - cursor.execute( - "SELECT property_name FROM property p JOIN entity e ON e.id=p.id WHERE e.id=%s AND e.status='active'", - (person_id,) + person_id = entity.enroll_person( + self.cur, "Person3", "pubkey_person", creator_id ) - props_active = [r['property_name'] for r in cursor.fetchall()] - self.assertNotIn("address", props_active) + entity.set_entity_status(self.cur, person_id, "revoked", creator_id) -if __name__ == "__main__": - unittest.main() + with self.assertRaises(ValueError): + property.set_property(self.cur, person_id, "prop3") + + with self.assertRaises(ValueError): + property.delete_property(self.cur, person_id, "prop3")