Cover all permission functions (59 tests), tree validation logic (25 tests), settings manager parse/infer helpers (21 tests), and Stripe webhook stubs (8 tests). Key modules now at 100% coverage: permissions.py, tree_validation.py, stripe_handlers.py. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
228 lines
7.2 KiB
Python
228 lines
7.2 KiB
Python
"""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)
|