"""Unit tests for tree validation logic. Tests validate_tree_structure, can_publish_tree, and edge cases. No database required. """ from app.core.tree_validation import ( TreeValidationError, can_publish_tree, validate_tree_structure, ) class TestValidateTreeStructure: def test_valid_solution_tree(self): valid, errors = validate_tree_structure({ "id": "root", "type": "solution", "solution": "Done" }) assert valid assert errors == [] def test_valid_decision_tree_with_branches(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Is it on?", "children": [ {"id": "yes", "type": "solution", "solution": "Great"}, {"id": "no", "type": "action", "action": "Turn it on"}, ], }) assert valid assert errors == [] def test_empty_structure_fails(self): valid, errors = validate_tree_structure({}) assert not valid assert any("empty" in e["message"].lower() for e in errors) def test_missing_id_on_root(self): valid, errors = validate_tree_structure({"type": "solution", "solution": "X"}) assert not valid assert any("id" in e["field"] for e in errors) def test_missing_type_on_root(self): valid, errors = validate_tree_structure({"id": "root"}) assert not valid assert any("type" in e["field"] for e in errors) def test_decision_missing_question(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision" }) assert not valid assert any("question" in e["message"].lower() for e in errors) def test_decision_with_empty_question(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "" }) assert not valid def test_decision_with_one_child_fails(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Q?", "children": [ {"id": "only", "type": "solution", "solution": "S"}, ], }) assert not valid assert any("2 branches" in e["message"] for e in errors) def test_decision_with_zero_children_passes(self): """Decision with no children is valid (leaf decision).""" valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Q?" }) assert valid def test_action_missing_action_field(self): valid, errors = validate_tree_structure({ "id": "root", "type": "action" }) assert not valid assert any("action" in e["message"].lower() for e in errors) def test_action_with_empty_action(self): valid, errors = validate_tree_structure({ "id": "root", "type": "action", "action": "" }) assert not valid def test_solution_missing_solution_field(self): valid, errors = validate_tree_structure({ "id": "root", "type": "solution" }) assert not valid assert any("solution" in e["message"].lower() for e in errors) def test_solution_with_empty_solution(self): valid, errors = validate_tree_structure({ "id": "root", "type": "solution", "solution": "" }) assert not valid def test_unknown_node_type(self): valid, errors = validate_tree_structure({ "id": "root", "type": "banana" }) assert not valid assert any("unknown" in e["message"].lower() for e in errors) def test_child_missing_id(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Q?", "children": [ {"type": "solution", "solution": "S1"}, {"id": "c2", "type": "solution", "solution": "S2"}, ], }) assert not valid assert any("id" in e["field"] for e in errors) def test_child_missing_type(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Q?", "children": [ {"id": "c1"}, {"id": "c2", "type": "solution", "solution": "S2"}, ], }) assert not valid assert any("type" in e["field"] for e in errors) def test_deeply_nested_validation(self): """Validates 3 levels deep.""" valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Level 1?", "children": [ { "id": "l2a", "type": "decision", "question": "Level 2?", "children": [ {"id": "l3a", "type": "solution", "solution": "Deep"}, {"id": "l3b", "type": "solution"}, # Missing solution ], }, {"id": "l2b", "type": "solution", "solution": "Shallow"}, ], }) assert not valid assert any("l3b" in str(e) or "children[1]" in e["field"] for e in errors) def test_multiple_errors_collected(self): valid, errors = validate_tree_structure({ "id": "root", "type": "decision", "question": "Q?", "children": [ {"id": "c1", "type": "solution"}, # missing solution {"id": "c2", "type": "action"}, # missing action ], }) assert not valid assert len(errors) >= 2 class TestCanPublishTree: def test_valid_tree_can_publish(self): can, errors = can_publish_tree( {"id": "root", "type": "solution", "solution": "Done"}, "My Tree" ) assert can assert errors == [] def test_empty_name_cannot_publish(self): can, errors = can_publish_tree( {"id": "root", "type": "solution", "solution": "Done"}, "" ) assert not can assert any("name" in e["field"] for e in errors) def test_whitespace_name_cannot_publish(self): can, errors = can_publish_tree( {"id": "root", "type": "solution", "solution": "Done"}, " " ) assert not can def test_none_name_cannot_publish(self): can, errors = can_publish_tree( {"id": "root", "type": "solution", "solution": "Done"}, None ) assert not can def test_invalid_structure_cannot_publish(self): can, errors = can_publish_tree({}, "My Tree") assert not can assert len(errors) >= 1 def test_both_name_and_structure_errors(self): can, errors = can_publish_tree({}, "") assert not can assert len(errors) >= 2 # name error + structure error class TestTreeValidationError: def test_exception_attributes(self): err = TreeValidationError("field.name", "is required") assert err.field == "field.name" assert err.message == "is required" assert "field.name" in str(err)