felt rettelser
This commit is contained in:
parent
7470cf7189
commit
a1be210f58
|
|
@ -0,0 +1,167 @@
|
|||
CA/PKI Backend Project Context
|
||||
Stack
|
||||
|
||||
Python 3 + psycopg (dict_row cursors)
|
||||
|
||||
PostgreSQL database: ca
|
||||
|
||||
Unit tests: unittest (python3 -m unittest discover)
|
||||
|
||||
Database Schema (current assumptions)
|
||||
entity
|
||||
|
||||
id INT identity PK
|
||||
|
||||
creation_ts TIMESTAMPTZ default now()
|
||||
|
||||
creator INT FK → entity(id) (the entity that created this one; nullable)
|
||||
|
||||
name VARCHAR(100) NOT NULL
|
||||
|
||||
type VARCHAR(...) NOT NULL (e.g. person, group, device, alias)
|
||||
|
||||
public_key VARCHAR(300) NOT NULL
|
||||
|
||||
symmetrical_key VARCHAR(100) NULL
|
||||
|
||||
status VARCHAR(...) NOT NULL default 'active' (values: 'active', 'revoked')
|
||||
|
||||
expiration DATE NULL
|
||||
|
||||
Index on entity(name) (and other indexes as needed)
|
||||
|
||||
group_member
|
||||
|
||||
group_id INT FK → entity(id) ON DELETE CASCADE
|
||||
|
||||
member_id INT FK → entity(id) ON DELETE CASCADE
|
||||
|
||||
role VARCHAR(10)
|
||||
|
||||
PK (group_id, member_id)
|
||||
|
||||
Index (member_id, group_id)
|
||||
|
||||
Groups can contain any entity type, including other groups and devices.
|
||||
|
||||
property
|
||||
|
||||
Columns: (id INT FK → entity(id), property_name VARCHAR(100))
|
||||
|
||||
PK (id, property_name)
|
||||
|
||||
Used for flags/roles such as "creator"
|
||||
|
||||
metadata
|
||||
|
||||
Intended “singleton row” table, enforced at application level
|
||||
|
||||
Columns: name, comment, private_key, public_key
|
||||
|
||||
log
|
||||
|
||||
id SERIAL PK
|
||||
|
||||
ts TIMESTAMPTZ default now()
|
||||
|
||||
entry TEXT NOT NULL
|
||||
|
||||
Every API mutation must log one row here.
|
||||
|
||||
Core Business Rules
|
||||
|
||||
Creators are not an entity type
|
||||
|
||||
creator is a property (property_name='creator') on a person entity.
|
||||
|
||||
insert_creator() creates a person entity and inserts the creator property.
|
||||
|
||||
Revoked entities are immutable
|
||||
|
||||
Any mutation on an entity requires ensure_entity_active(cursor, entity_id).
|
||||
|
||||
Revoked entities cannot:
|
||||
|
||||
Join groups or accept members
|
||||
|
||||
Add/delete properties
|
||||
|
||||
Change keys (public_key, symmetrical_key)
|
||||
|
||||
Change status again
|
||||
|
||||
Logging
|
||||
|
||||
All changes to entity, group_member, property, metadata must call log_change(cursor, "...")
|
||||
|
||||
Logging happens inside the same transaction (no extra commits).
|
||||
|
||||
Python Modules (current structure)
|
||||
|
||||
ca_core/entity.py
|
||||
|
||||
Must provide:
|
||||
|
||||
ensure_entity_active(cursor, entity_id)
|
||||
|
||||
insert_creator(cursor, name, public_key)
|
||||
|
||||
enroll_person(cursor, name, public_key, creator_id)
|
||||
|
||||
create_group(cursor, name, public_key, creator_id)
|
||||
|
||||
create_alias(cursor, target_entity_id)
|
||||
|
||||
get_entity(cursor, entity_id)
|
||||
|
||||
set_entity_status(cursor, entity_id, status, changed_by) (requires active entity)
|
||||
|
||||
set_entity_keys(cursor, entity_id, public_key, changed_by) (active-only)
|
||||
|
||||
set_symmetrical_key(cursor, entity_id, key, changed_by) (active-only)
|
||||
|
||||
get_symmetrical_key(cursor, entity_id)
|
||||
|
||||
ca_core/group_member.py
|
||||
|
||||
Uses member_id (not person_id)
|
||||
|
||||
Must prevent adding revoked groups/members (via ensure_entity_active)
|
||||
|
||||
Logs add/remove membership
|
||||
|
||||
ca_core/property.py
|
||||
|
||||
Table is property(id, property_name) (NOT entity_id/name)
|
||||
|
||||
Must reject mutations if entity revoked (immutability)
|
||||
|
||||
Logs set/delete property
|
||||
|
||||
ca_core/metadata.py
|
||||
|
||||
Updates metadata fields and logs changes
|
||||
|
||||
ca_core/db_logging.py
|
||||
|
||||
log_change(cursor, message: str) inserts into log(entry)
|
||||
|
||||
Tests
|
||||
|
||||
tests/test_entity.py, tests/test_group.py, tests/test_property.py, tests/test_metadata.py
|
||||
|
||||
Tests verify:
|
||||
|
||||
Core behaviors (create, enroll, group membership, revoke immutability)
|
||||
|
||||
Log entry is created for mutations (case-insensitive substring checks)
|
||||
|
||||
Run via:
|
||||
|
||||
python3 -m unittest discover
|
||||
|
||||
Known gotchas
|
||||
|
||||
Avoid naming a module logging.py (conflicts with stdlib). Use db_logging.py.
|
||||
|
||||
Schema and code must stay aligned (e.g., property.id/property_name, group_member.member_id).
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -14,14 +14,25 @@ def ensure_entity_active(cursor, entity_id):
|
|||
raise ValueError("Entity is not active")
|
||||
|
||||
|
||||
def _validate_ca_reference_for_group(ca_reference):
|
||||
if ca_reference is None:
|
||||
raise ValueError("ca_reference is required for groups")
|
||||
if not isinstance(ca_reference, str):
|
||||
raise ValueError("ca_reference must be a string")
|
||||
if len(ca_reference) == 0:
|
||||
raise ValueError("ca_reference cannot be empty")
|
||||
if len(ca_reference) > 100:
|
||||
raise ValueError("ca_reference must be at most 100 characters")
|
||||
|
||||
|
||||
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, status)
|
||||
VALUES (%s, 'person', %s, 'active')
|
||||
INSERT INTO entity (name, type, public_key, status, ca_reference)
|
||||
VALUES (%s, 'person', %s, 'active', NULL)
|
||||
RETURNING id
|
||||
""",
|
||||
(name, public_key),
|
||||
|
|
@ -47,8 +58,8 @@ def enroll_person(cursor, name, public_key, creator_id):
|
|||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO entity (name, type, public_key, creator, status)
|
||||
VALUES (%s, 'person', %s, %s, 'active')
|
||||
INSERT INTO entity (name, type, public_key, creator, status, ca_reference)
|
||||
VALUES (%s, 'person', %s, %s, 'active', NULL)
|
||||
RETURNING id
|
||||
""",
|
||||
(name, public_key, creator_id),
|
||||
|
|
@ -59,38 +70,25 @@ def enroll_person(cursor, name, public_key, creator_id):
|
|||
return person_id
|
||||
|
||||
|
||||
def create_group(cursor, name, public_key, creator_id):
|
||||
def create_group(cursor, name, public_key, creator_id, ca_reference):
|
||||
ensure_entity_active(cursor, creator_id)
|
||||
_validate_ca_reference_for_group(ca_reference)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO entity (name, type, public_key, creator, status)
|
||||
VALUES (%s, 'group', %s, %s, 'active')
|
||||
INSERT INTO entity (name, type, public_key, creator, status, ca_reference)
|
||||
VALUES (%s, 'group', %s, %s, 'active', %s)
|
||||
RETURNING id
|
||||
""",
|
||||
(name, public_key, creator_id),
|
||||
(name, public_key, creator_id, ca_reference),
|
||||
)
|
||||
group_id = cursor.fetchone()["id"]
|
||||
|
||||
log_change(cursor, f"Created group {group_id} under creator {creator_id}")
|
||||
return group_id
|
||||
|
||||
|
||||
def create_alias(cursor, target_entity_id):
|
||||
ensure_entity_active(cursor, target_entity_id)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO entity (name, type, creator, status)
|
||||
VALUES (%s, 'alias', %s, 'active')
|
||||
RETURNING id
|
||||
""",
|
||||
(f"alias_for_{target_entity_id}", target_entity_id),
|
||||
log_change(
|
||||
cursor,
|
||||
f"Created group {group_id} under creator {creator_id} with ca_reference {ca_reference}",
|
||||
)
|
||||
alias_id = cursor.fetchone()["id"]
|
||||
|
||||
log_change(cursor, f"Created alias {alias_id} for entity {target_entity_id}")
|
||||
return alias_id
|
||||
return group_id
|
||||
|
||||
|
||||
def get_entity(cursor, entity_id):
|
||||
|
|
|
|||
|
|
@ -54,3 +54,24 @@ def get_private_key(cursor):
|
|||
row = cursor.fetchone()
|
||||
return row["private_key"] if row else None
|
||||
|
||||
|
||||
def set_defense_p(cursor, defense_p: bool):
|
||||
"""Set the metadata defense_p flag.
|
||||
|
||||
This table is treated as a singleton row at the application level.
|
||||
Current convention in this codebase is to wipe and re-insert.
|
||||
"""
|
||||
cursor.execute("DELETE FROM metadata")
|
||||
cursor.execute(
|
||||
"INSERT INTO metadata (defense_p) VALUES (%s)",
|
||||
(defense_p,)
|
||||
)
|
||||
log_change(cursor, f"Updated metadata defense_p to {defense_p}")
|
||||
|
||||
|
||||
def get_defense_p(cursor) -> bool:
|
||||
cursor.execute("SELECT defense_p FROM metadata LIMIT 1")
|
||||
row = cursor.fetchone()
|
||||
if not row or row["defense_p"] is None:
|
||||
return False
|
||||
return bool(row["defense_p"])
|
||||
|
|
|
|||
|
|
@ -2,25 +2,71 @@ from db_logging import log_change
|
|||
from entity import ensure_entity_active
|
||||
|
||||
|
||||
def set_property(cursor, entity_id, property_name):
|
||||
def _validate_validation_policy(validation_policy: str) -> str:
|
||||
if validation_policy is None:
|
||||
return "default"
|
||||
if not isinstance(validation_policy, str):
|
||||
raise TypeError("validation_policy must be a string")
|
||||
vp = validation_policy.strip()
|
||||
if not vp:
|
||||
raise ValueError("validation_policy cannot be empty")
|
||||
if len(vp) > 19:
|
||||
raise ValueError("validation_policy must be at most 19 characters")
|
||||
return vp
|
||||
|
||||
|
||||
def _validate_source(source):
|
||||
if source is None:
|
||||
return None
|
||||
if not isinstance(source, str):
|
||||
raise TypeError("source must be a string or None")
|
||||
s = source.strip()
|
||||
if len(s) > 150:
|
||||
raise ValueError("source must be at most 150 characters")
|
||||
# Allow empty string -> treat as NULL for cleanliness
|
||||
return s if s else None
|
||||
|
||||
|
||||
def set_property(cursor, entity_id, property_name, validation_policy="default", source=None):
|
||||
"""
|
||||
Revoked entities are immutable: cannot add properties.
|
||||
Schema: property(id, property_name)
|
||||
Revoked entities are immutable: cannot add/update properties.
|
||||
|
||||
Schema: property(id, property_name, validation_policy, source)
|
||||
- validation_policy: CHAR(19) NOT NULL DEFAULT 'default'
|
||||
- source: VARCHAR(150) NULL
|
||||
"""
|
||||
ensure_entity_active(cursor, entity_id)
|
||||
|
||||
if not isinstance(property_name, str) or not property_name.strip():
|
||||
raise ValueError("property_name must be a non-empty string")
|
||||
if len(property_name) > 100:
|
||||
raise ValueError("property_name must be at most 100 characters")
|
||||
|
||||
vp = _validate_validation_policy(validation_policy)
|
||||
src = _validate_source(source)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO property (id, property_name)
|
||||
VALUES (%s, %s)
|
||||
ON CONFLICT (id, property_name) DO NOTHING
|
||||
INSERT INTO property (id, property_name, validation_policy, source)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (id, property_name)
|
||||
DO UPDATE SET
|
||||
validation_policy = EXCLUDED.validation_policy,
|
||||
source = EXCLUDED.source
|
||||
""",
|
||||
(entity_id, property_name),
|
||||
(entity_id, property_name, vp, src),
|
||||
)
|
||||
log_change(
|
||||
cursor,
|
||||
f"Set property '{property_name}' for entity {entity_id} "
|
||||
f"(validation_policy={vp}, source={src})",
|
||||
)
|
||||
log_change(cursor, f"Set property '{property_name}' for entity {entity_id}")
|
||||
|
||||
|
||||
def get_properties(cursor, entity_id):
|
||||
"""
|
||||
Returns a list of property_name values for the entity.
|
||||
"""
|
||||
cursor.execute(
|
||||
"SELECT property_name FROM property WHERE id = %s",
|
||||
(entity_id,),
|
||||
|
|
@ -29,6 +75,22 @@ def get_properties(cursor, entity_id):
|
|||
return [r["property_name"] for r in rows]
|
||||
|
||||
|
||||
def get_property(cursor, entity_id, property_name):
|
||||
"""
|
||||
Returns a dict_row with keys: property_name, validation_policy, source
|
||||
or None if not found.
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT property_name, validation_policy, source
|
||||
FROM property
|
||||
WHERE id = %s AND property_name = %s
|
||||
""",
|
||||
(entity_id, property_name),
|
||||
)
|
||||
return cursor.fetchone()
|
||||
|
||||
|
||||
def delete_property(cursor, entity_id, property_name):
|
||||
"""
|
||||
Revoked entities are immutable: cannot delete properties.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
-- ------------------------
|
||||
-- Metadata
|
||||
-- Metadata (singleton row; enforced at application level)
|
||||
-- ------------------------
|
||||
DROP TABLE IF EXISTS metadata;
|
||||
|
||||
|
|
@ -7,7 +7,8 @@ CREATE TABLE metadata(
|
|||
name VARCHAR(50),
|
||||
comment VARCHAR(200),
|
||||
private_key VARCHAR(500),
|
||||
public_key VARCHAR(500)
|
||||
public_key VARCHAR(500),
|
||||
defense_p BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
-- ------------------------
|
||||
|
|
@ -23,8 +24,14 @@ CREATE TABLE entity(
|
|||
type VARCHAR(20) NOT NULL, -- person, group, device
|
||||
symmetrical_key VARCHAR(100),
|
||||
public_key VARCHAR(300) NOT NULL,
|
||||
ca_reference VARCHAR(100),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
expiration DATE
|
||||
expiration DATE,
|
||||
CONSTRAINT entity_ca_reference_check CHECK (
|
||||
(type = 'group' AND ca_reference IS NOT NULL)
|
||||
OR
|
||||
(type <> 'group' AND ca_reference IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_entity_name ON entity(name);
|
||||
|
|
@ -50,7 +57,9 @@ DROP TABLE IF EXISTS property;
|
|||
|
||||
CREATE TABLE property(
|
||||
id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE,
|
||||
property_name VARCHAR(100),
|
||||
property_name VARCHAR(100) NOT NULL,
|
||||
validation_policy CHAR(19) NOT NULL DEFAULT 'default',
|
||||
source VARCHAR(150),
|
||||
PRIMARY KEY (id, property_name)
|
||||
);
|
||||
|
||||
|
|
@ -66,4 +75,3 @@ CREATE TABLE log(
|
|||
);
|
||||
|
||||
CREATE INDEX idx_log_ts ON log(ts);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
drop table metadata;
|
||||
|
||||
create table metadata(
|
||||
name varchar(50),
|
||||
comment varchar(200),
|
||||
private_key varchar(500),
|
||||
public_key varchar(500)
|
||||
);
|
||||
|
||||
insert into metadata default vALUES;
|
||||
|
||||
drop table entity cascade;
|
||||
|
||||
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,
|
||||
group_p BOOLEAN NOT NULL,
|
||||
geo_offset BIGINT,
|
||||
--private_key VARCHAR(300) NOT NULL,
|
||||
public_key VARCHAR(300) NOT NULL,
|
||||
expiration DATE,
|
||||
status VARCHAR(10) NOT NULL DEFAULT 'active'
|
||||
|
||||
|
||||
);
|
||||
|
||||
CREATE INDEX idx_entity_name ON entity(name);
|
||||
|
||||
drop table group_member;
|
||||
|
||||
create table group_member(
|
||||
group_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE,
|
||||
person_id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE,
|
||||
role VARCHAR(10),
|
||||
PRIMARY KEY (group_id,person_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_group_member_person_group ON group_member(person_id, group_id);
|
||||
|
||||
drop table property;
|
||||
|
||||
create table property(
|
||||
id INT NOT NULL REFERENCES entity(id) ON DELETE CASCADE,
|
||||
property_name VARCHAR(100),
|
||||
PRIMARY KEY (id, property_name)
|
||||
);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -38,6 +38,7 @@ class TestEntityFunctions(unittest.TestCase):
|
|||
creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1")
|
||||
row = entity.get_entity(self.cur, creator_id)
|
||||
self.assertEqual(row["name"], "Creator1")
|
||||
self.assertIsNone(row["ca_reference"])
|
||||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("creator entity", log_entry)
|
||||
|
|
@ -47,17 +48,30 @@ class TestEntityFunctions(unittest.TestCase):
|
|||
creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2")
|
||||
person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id)
|
||||
|
||||
row = entity.get_entity(self.cur, person_id)
|
||||
self.assertIsNone(row["ca_reference"])
|
||||
|
||||
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):
|
||||
def test_create_group_requires_ca_reference(self):
|
||||
creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3")
|
||||
group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
entity.create_group(self.cur, "GroupMissingRef", "pubkey_group", creator_id, None)
|
||||
|
||||
def test_create_group_sets_ca_reference(self):
|
||||
creator_id = entity.insert_creator(self.cur, "Creator4", "pubkey4")
|
||||
group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id, "CA-REF-1")
|
||||
|
||||
row = entity.get_entity(self.cur, group_id)
|
||||
self.assertEqual(row["ca_reference"], "CA-REF-1")
|
||||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("created group", log_entry)
|
||||
self.assertIn(str(group_id), log_entry)
|
||||
self.assertIn("ca-ref-1".lower(), log_entry)
|
||||
|
||||
def test_set_and_get_symmetrical_key(self):
|
||||
creator_id = entity.insert_creator(self.cur, "CreatorSym", "pubkey_sym")
|
||||
|
|
@ -68,4 +82,3 @@ class TestEntityFunctions(unittest.TestCase):
|
|||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("symmetrical_key", log_entry)
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ class TestGroupFunctions(unittest.TestCase):
|
|||
def test_add_and_get_members(self):
|
||||
creator_id = entity.insert_creator(self.cur, "Creator1", "pubkey1")
|
||||
person_id = entity.enroll_person(self.cur, "Person1", "pubkey_person", creator_id)
|
||||
group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id)
|
||||
group_id = entity.create_group(self.cur, "Group1", "pubkey_group", creator_id, "CA-GROUP-1")
|
||||
|
||||
group_member.add_group_member(self.cur, group_id, person_id, "member")
|
||||
|
||||
|
|
@ -55,8 +55,8 @@ class TestGroupFunctions(unittest.TestCase):
|
|||
|
||||
def test_nested_groups(self):
|
||||
creator_id = entity.insert_creator(self.cur, "Creator2", "pubkey2")
|
||||
parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_parent", creator_id)
|
||||
child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_child", creator_id)
|
||||
parent_group = entity.create_group(self.cur, "ParentGroup", "pubkey_parent", creator_id, "CA-PARENT")
|
||||
child_group = entity.create_group(self.cur, "ChildGroup", "pubkey_child", creator_id, "CA-CHILD")
|
||||
|
||||
group_member.add_group_member(self.cur, parent_group, child_group, "subgroup")
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ class TestGroupFunctions(unittest.TestCase):
|
|||
|
||||
def test_revoked_group_cannot_accept_members(self):
|
||||
creator_id = entity.insert_creator(self.cur, "Creator3", "pubkey3")
|
||||
group_id = entity.create_group(self.cur, "RevokedGroup", "pubkey_group", creator_id)
|
||||
group_id = entity.create_group(self.cur, "RevokedGroup", "pubkey_group", creator_id, "CA-REVOKED-GROUP")
|
||||
person_id = entity.enroll_person(self.cur, "Person2", "pubkey_person", creator_id)
|
||||
|
||||
entity.set_entity_status(self.cur, group_id, "revoked", creator_id)
|
||||
|
|
@ -79,11 +79,10 @@ class TestGroupFunctions(unittest.TestCase):
|
|||
|
||||
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)
|
||||
group_id = entity.create_group(self.cur, "ActiveGroup", "pubkey_group", creator_id, "CA-ACTIVE-GROUP")
|
||||
person_id = entity.enroll_person(self.cur, "RevokedPerson", "pubkey_person", creator_id)
|
||||
|
||||
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")
|
||||
|
||||
|
|
|
|||
|
|
@ -58,3 +58,9 @@ class TestMetadataFunctions(unittest.TestCase):
|
|||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("metadata keys", log_entry)
|
||||
|
||||
def test_set_and_get_defense_p(self):
|
||||
metadata.set_defense_p(self.cur, True)
|
||||
self.assertEqual(metadata.get_defense_p(self.cur), True)
|
||||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("defense_p", log_entry)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ class TestPropertyFunctions(unittest.TestCase):
|
|||
props = property.get_properties(self.cur, person_id)
|
||||
self.assertIn("prop1", props)
|
||||
|
||||
details = property.get_property(self.cur, person_id, "prop1")
|
||||
self.assertIsNotNone(details)
|
||||
# CHAR(19) is space-padded in PostgreSQL; strip for comparison.
|
||||
self.assertEqual(details["validation_policy"].strip(), "default")
|
||||
self.assertIsNone(details["source"])
|
||||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("set property", log_entry)
|
||||
self.assertIn("prop1", log_entry)
|
||||
|
|
@ -76,6 +82,32 @@ class TestPropertyFunctions(unittest.TestCase):
|
|||
self.assertIn("deleted property", log_entry)
|
||||
self.assertIn("prop2", log_entry)
|
||||
|
||||
|
||||
def test_set_property_with_policy_and_source(self):
|
||||
creator_id = entity.insert_creator(self.cur, "CreatorPolicy", "pubkey_policy")
|
||||
person_id = entity.enroll_person(
|
||||
self.cur, "PersonPolicy", "pubkey_person", creator_id
|
||||
)
|
||||
|
||||
property.set_property(
|
||||
self.cur,
|
||||
person_id,
|
||||
"prop_policy",
|
||||
validation_policy="strict",
|
||||
source="unit-test",
|
||||
)
|
||||
|
||||
details = property.get_property(self.cur, person_id, "prop_policy")
|
||||
self.assertIsNotNone(details)
|
||||
self.assertEqual(details["validation_policy"].strip(), "strict")
|
||||
self.assertEqual(details["source"], "unit-test")
|
||||
|
||||
log_entry = get_last_log(self.cur).lower()
|
||||
self.assertIn("set property", log_entry)
|
||||
self.assertIn("prop_policy", log_entry)
|
||||
self.assertIn("strict", log_entry)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Immutability: revoked entity cannot mutate properties
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -93,4 +125,3 @@ class TestPropertyFunctions(unittest.TestCase):
|
|||
|
||||
with self.assertRaises(ValueError):
|
||||
property.delete_property(self.cur, person_id, "prop3")
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue