- Timestamp:
- 11/27/25 18:12:23 (7 weeks ago)
- Branches:
- master
- Children:
- 4e11d58
- Parents:
- 9a50e0b
- Location:
- tests
- Files:
-
- 5 edited
-
conftest.py (modified) (4 diffs)
-
test_flexoid.py (modified) (1 diff)
-
test_id_lifecycle.py (modified) (6 diffs)
-
test_id_stress.py (modified) (3 diffs)
-
test_persistance_integrity.py (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
tests/conftest.py
r9a50e0b ref964d8 1 # tests/stubs/single_choice_question.py2 from dataclasses import dataclass, field3 1 import pytest 4 2 import platform 5 3 from pathlib import Path 6 4 from datetime import datetime 7 from typing import List8 from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState, Domain,get_signing_backend, CertificateReference5 from flexoentity import Domain, FlexoSignature 6 from flexoentity import get_signing_backend, CertificateReference 9 7 10 8 … … 18 16 return FixedDate 19 17 20 @dataclass21 class AnswerOption:22 id: str23 text: str24 points: float = 0.025 26 def to_dict(self):27 return {"id": self.id, "text": self.text, "points": self.points}28 29 @classmethod30 def from_dict(cls, data):31 return cls(32 id=data.get("id", ""),33 text=data.get("text", ""),34 points=data.get("points", 0.0)35 )36 37 38 @dataclass39 class SingleChoiceQuestion(FlexoEntity):40 """A minimal stub to test FlexoEntity integration."""41 ENTITY_TYPE = EntityType.ITEM42 43 text: str = ""44 options: List[AnswerOption] = field(default_factory=list)45 46 def __post_init__(self):47 # If no FlexOID yet, generate a draft ID now.48 if not getattr(self, "flexo_id", None):49 self.flexo_id = FlexOID.safe_generate(50 domain_id=self.domain_id,51 entity_type=SingleChoiceQuestion.ENTITY_TYPE.value, # 'I'52 state=EntityState.DRAFT.value, # 'D'53 text=self.text_seed or self.text,54 version=1,55 )56 57 @classmethod58 def default(cls):59 return cls()60 61 def _serialize_content(self):62 return {63 "text": self.text,64 "options": [opt.to_dict() for opt in self.options],65 }66 67 @property68 def text_seed(self) -> str:69 """Include answer options (and points) for deterministic ID generation."""70 71 joined = "|".join(72 f"{opt.text.strip()}:{opt.points}"73 for opt in sorted(self.options, key=lambda o: o.text.strip().lower())74 )75 return f"{self.text}{joined}"76 77 @classmethod78 def from_dict(cls, data):79 obj = cls(text=data.get("text", ""),80 options=[AnswerOption.from_dict(o) for o in data.get("options", [])],81 )82 # restore FlexoEntity core fields83 if "flexo_id" in data:84 obj.flexo_id = FlexOID.to_dict(data["flexo_id"])85 return obj86 18 87 19 @pytest.fixture … … 92 24 description="ALL ABOUT ARITHMETIC IN PYTHON") 93 25 94 @pytest.fixture95 def sample_question(sample_domain):96 q = SingleChoiceQuestion.with_domain_id(domain_id=sample_domain.domain_id,97 text="What is 2 + 2?",98 options=[])99 q._update_fingerprint()100 return q101 26 102 27 SYSTEM = platform.system() … … 181 106 except Exception as e: 182 107 pytest.skip(f"Backend unavailable or misconfigured: {e}") 108 109 @pytest.fixture 110 def sample_signature(sample_domain, cert_ref_linux): 111 return FlexoSignature.with_domain_id(domain_id="SIG", signed_entity=sample_domain, 112 certificate_reference=cert_ref_linux, 113 comment="This is a mock signature") 114 -
tests/test_flexoid.py
r9a50e0b ref964d8 111 111 # ────────────────────────────────────────────── 112 112 113 def test_canonical_seed_for_strings _and_dicts():113 def test_canonical_seed_for_strings(): 114 114 s1 = " Hello world " 115 115 s2 = "Hello world" 116 116 assert canonical_seed(s1) == canonical_seed(s2) 117 118 d1 = {"b": 1, "a": 2}119 d2 = {"a": 2, "b": 1}120 assert canonical_seed(d1) == canonical_seed(d2)121 122 class Dummy:123 def __init__(self):124 self.x = 1125 self.y = 2126 obj = Dummy()127 assert isinstance(canonical_seed(obj), str)128 129 117 130 118 # ────────────────────────────────────────────── -
tests/test_id_lifecycle.py
r9a50e0b ref964d8 3 3 4 4 5 # ────────────────────────────────────────────────────────────────────────────── 6 # Tests adapted to use real SingleChoiceQuestion fixture instead of DummyEntity 7 # ────────────────────────────────────────────────────────────────────────────── 5 def test_initial_state(sample_domain): 6 assert sample_domain.state == EntityState.DRAFT 7 assert sample_domain.flexo_id.version == 1 8 assert FlexoEntity.verify_integrity(sample_domain) 8 9 9 def test_initial_state(sample_question): 10 q = sample_question 11 assert q.state == EntityState.DRAFT 12 assert q.flexo_id.version == 1 13 assert FlexoEntity.verify_integrity(q) 14 15 def test_approval_does_not_bump_version(sample_question): 16 q = sample_question 10 def test_approval_does_not_bump_version(sample_domain): 11 q = sample_domain 17 12 q.approve() 18 13 assert q.state == EntityState.APPROVED 19 14 assert q.flexo_id.version == 1 20 15 21 def test_signing_does_not_bump_version(sample_ question):22 q = sample_ question16 def test_signing_does_not_bump_version(sample_domain): 17 q = sample_domain 23 18 q.approve() 24 19 before = q.flexo_id … … 38 33 39 34 40 def test_publish_does_not_bump_version(sample_ question):41 q = sample_ question35 def test_publish_does_not_bump_version(sample_domain): 36 q = sample_domain 42 37 q.approve() 43 38 q.sign() … … 48 43 49 44 50 def test_modify_content_changes_fingerprint(sample_question): 51 q = sample_question 52 q.text += "Rephrased content" # simulate text change 53 changed = q._update_fingerprint() 45 def test_modify_content_changes_fingerprint(sample_signature): 46 sample_signature.comment += "Rephrased content" # simulate text change 47 changed = sample_signature._update_fingerprint() 54 48 assert changed 55 49 56 50 57 def test_no_version_bump_on_draft_edits(sample_question): 58 q = sample_question 59 q.text = "Minor draft edit" 60 q._update_fingerprint() 61 assert q.flexo_id.version == 1 51 def test_no_version_bump_on_draft_edits(sample_signature): 52 sample_signature.comment = "Minor draft edit" 53 sample_signature._update_fingerprint() 54 assert sample_signature.flexo_id.version == 1 62 55 63 56 64 def test_version_bump_after_edit_and_sign(sample_question): 65 q = sample_question 66 q.approve() 67 v1 = str(q.flexo_id) 68 q.text = "Changed content" 69 q.sign() 70 assert str(q.flexo_id) != v1 57 def test_version_bump_after_edit_and_sign(sample_signature): 58 sample_signature.approve() 59 v1 = str(sample_signature.flexo_id) 60 sample_signature.comment = "Changed comment" 61 sample_signature.sign() 62 assert str(sample_signature.flexo_id) != v1 71 63 72 64 73 def test_integrity_check_passes_and_fails(sample_question): 74 q = sample_question 75 q.approve() 76 assert FlexoEntity.verify_integrity(q) 65 def test_integrity_check_passes_and_fails(sample_signature): 66 sample_signature.approve() 67 assert FlexoEntity.verify_integrity(sample_signature) 77 68 78 69 # simulate tampering 79 q.text = "Tampered text"80 assert not FlexoEntity.verify_integrity( q)70 sample_signature.comment = "Tampered text" 71 assert not FlexoEntity.verify_integrity(sample_signature) 81 72 82 73 83 def test_obsolete_state(sample_ question):84 q = sample_ question74 def test_obsolete_state(sample_domain): 75 q = sample_domain 85 76 q.approve() 86 77 q.sign() … … 90 81 91 82 92 def test_clone_new_base_resets_lineage(sample_ question):93 q = sample_ question83 def test_clone_new_base_resets_lineage(sample_domain): 84 q = sample_domain 94 85 q.approve() 95 86 q.sign() … … 102 93 assert q.flexo_id.version == 1 103 94 104 def test_clone_new_base_sets_origin(sample_ question):105 q = sample_ question95 def test_clone_new_base_sets_origin(sample_domain): 96 q = sample_domain 106 97 q.approve() 107 98 q.sign() … … 115 106 assert q.flexo_id != old_id 116 107 117 def test_mass_version_increments_until_obsolete(sample_ question):118 q = sample_ question108 def test_mass_version_increments_until_obsolete(sample_domain): 109 q = sample_domain 119 110 q.approve() 120 111 for _ in range(FlexOID.MAX_VERSION - 1): -
tests/test_id_stress.py
r9a50e0b ref964d8 9 9 10 10 import pytest 11 12 from flexoentity import FlexOID, EntityType, EntityState 11 from uuid import uuid4 12 from flexoentity import FlexOID, EntityType, EntityState, FlexoSignature 13 13 14 14 logger = logging.getLogger(__name__) … … 66 66 assert id1 == id2 67 67 68 def test_massive_lifecycle_simulation(cert_ref_linux, sample_domain): 69 """ 70 Generate 100 random FlexoSignatures, mutate content, run through lifecycle, 71 and ensure all FlexOIDs are unique and valid. 72 """ 73 entities = [] 68 74 69 def test_massive_lifecycle_simulation(sample_question): 70 """ 71 Generate 100 random SingleChoiceQuestions, simulate multiple edits and state transitions, 72 ensure all final IDs and fingerprints are unique and valid. 73 """ 74 entities = [ 75 copy.deepcopy(sample_question) for _ in range(100) 76 ] 75 for i in range(100): 76 sig = FlexoSignature.with_domain_id( 77 domain_id="SIGTEST", 78 signed_entity=sample_domain.flexo_id, 79 signer_id=uuid4(), 80 certificate_reference=cert_ref_linux, 81 comment=f"Initial signature #{i}" 82 ) 83 entities.append(sig) 77 84 85 # Mutate + lifecycle transitions 78 86 for i, e in enumerate(entities): 79 # random edit80 e. text += f" updated #{i}"87 # CONTENT CHANGE → fingerprint changes → hash → FlexOID.prefix changes 88 e.comment += f" updated-{i}" 81 89 e._update_fingerprint() 82 90 … … 88 96 e.publish() 89 97 90 flexoids = [e.flexo_id for e in entities] 91 assert len(flexoids) == len(set(flexoids)), "Duplicate FlexOIDs after lifecycle simulation" 98 # Check ID uniqueness 99 flexoids = [str(e.flexo_id) for e in entities] 100 assert len(flexoids) == len(set(flexoids)), "Duplicate FlexOIDs detected" -
tests/test_persistance_integrity.py
r9a50e0b ref964d8 6 6 import pytest 7 7 8 from flexoentity import EntityState, EntityType, Domain 8 from flexoentity import Domain 9 9 10 10 11 @pytest.fixture 11 def approved_question(): 12 """Provide a fully approved and published SingleChoiceQuestion for persistence tests.""" 13 q = SingleChoiceQuestion( 14 domain=Domain(domain="GEN", entity_type=EntityType.DOMAIN, state=EntityState.DRAFT), 15 entity_type=None, # SingleChoiceQuestion sets this internally to EntityType.ITEM 16 state=EntityState.DRAFT, 17 text="What is Ohm’s law?", 18 options=[ 19 AnswerOption(id="OP1", text="U = R × I", points=1), 20 AnswerOption(id="OP2", text="U = I / R", points=0), 21 AnswerOption(id="OP3", text="R = U × I", points=0), 22 ], 23 ) 24 q.approve() 25 q.sign() 26 q.publish() 27 return q 12 def approved_domain(sample_domain): 13 """Provide a fully approved and published Domain for persistence tests.""" 14 sample_domain.approve() 15 sample_domain.sign() 16 sample_domain.publish() 17 return sample_domain 28 18 29 19 30 @pytest.mark.skip(reason="FlexOIDs regenerated on import; enable once JSON format is stable") 31 def test_json_roundtrip_preserves_integrity(approved_question): 20 def test_json_roundtrip_preserves_integrity(approved_domain): 32 21 """ 33 22 Export to JSON and reload — ensure fingerprints and signatures remain valid. 34 23 """ 35 json_str = approved_ question.to_json()36 loaded = SingleChoiceQuestion.from_json(json_str)24 json_str = approved_domain.to_json() 25 loaded = Domain.from_json(json_str) 37 26 38 27 # Fingerprint and state should match — integrity must pass 39 assert SingleChoiceQuestion.verify_integrity(loaded)28 assert Domain.verify_integrity(loaded) 40 29 41 30 # Metadata should be preserved exactly 42 assert approved_ question.fingerprint == loaded.fingerprint43 assert approved_ question.flexo_id == loaded.flexo_id44 assert loaded.state == approved_ question.state31 assert approved_domain.fingerprint == loaded.fingerprint 32 assert approved_domain.flexo_id == loaded.flexo_id 33 assert loaded.state == approved_domain.state 45 34 46 @pytest.mark.skip(reason="FlexOIDs regenerated on import; tampering detection not yet implemented") 47 def test_json_tampering_detection(approved_ question):35 36 def test_json_tampering_detection(approved_domain): 48 37 """Tampering with content should invalidate fingerprint verification.""" 49 json_str = approved_ question.to_json()38 json_str = approved_domain.to_json() 50 39 tampered = json.loads(json_str) 51 tampered[" text"] = "Tampered content injection"40 tampered["content"]["fullname"] = "Tampered content injection" 52 41 tampered_json = json.dumps(tampered) 53 42 54 loaded = SingleChoiceQuestion.from_json(tampered_json)55 assert not SingleChoiceQuestion.verify_integrity(loaded)43 loaded = Domain.from_json(tampered_json) 44 assert not Domain.verify_integrity(loaded) 56 45 57 @pytest.mark.skip(reason="FlexOIDs regenerated on import; corruption detection not yet applicable") 58 def test_json_file_corruption(approved_ question, tmp_path):46 47 def test_json_file_corruption(approved_domain, tmp_path): 59 48 """Simulate file corruption — integrity check must fail.""" 60 49 file = tmp_path / "question.json" 61 json_str = approved_question.to_json() 50 json_str = approved_domain.to_json() 51 print("JSON", json_str) 62 52 file.write_text(json_str) 63 53 64 54 # Corrupt the file (simulate accidental byte modification) 65 corrupted = json_str.replace(" Ohm’s", "Omm’s")55 corrupted = json_str.replace("ARITHMETIC", "ARITHM") 66 56 file.write_text(corrupted) 67 57 68 loaded = SingleChoiceQuestion.from_json(file.read_text())69 assert not SingleChoiceQuestion.verify_integrity(loaded)58 loaded = Domain.from_json(file.read_text()) 59 assert not Domain.verify_integrity(loaded)
Note:
See TracChangeset
for help on using the changeset viewer.
