""" Tests for Phase 7: Space Composability. Tests space references, variable/config inheritance, and access control. """ import pytest import tempfile from pathlib import Path from datetime import datetime, timedelta from markitect.spaces import ( # Models InformationSpace, SpaceConfig, SpaceMetadata, SpaceVariable, SpaceStatus, # Composability Models SpaceReference, SpaceReferenceType, SpacePermission, SpaceRole, AccessLevel, SpaceAccess, InheritedVariable, InheritedConfig, # Services ComposableSpaceService, InheritanceResolver, AccessControlService, CircularReferenceError, # Repositories SqliteSpaceRepository, SqliteVariableRepository, SqliteSpaceReferenceRepository, SqliteAccessControlRepository, ) # =========================================================================== # Fixtures # =========================================================================== @pytest.fixture def db_path(): """Create a temporary database path.""" with tempfile.TemporaryDirectory() as tmpdir: yield Path(tmpdir) / "test.db" @pytest.fixture def space_repo(db_path): """Create a space repository.""" return SqliteSpaceRepository(db_path) @pytest.fixture def variable_repo(db_path): """Create a variable repository.""" return SqliteVariableRepository(db_path) @pytest.fixture def reference_repo(db_path): """Create a reference repository.""" return SqliteSpaceReferenceRepository(db_path) @pytest.fixture def permission_repo(db_path): """Create a permission repository.""" return SqliteAccessControlRepository(db_path) @pytest.fixture def inheritance_resolver(space_repo, variable_repo, reference_repo): """Create an inheritance resolver.""" return InheritanceResolver( space_repo=space_repo, variable_repo=variable_repo, reference_repo=reference_repo, ) @pytest.fixture def access_service(permission_repo, space_repo, inheritance_resolver): """Create an access control service.""" return AccessControlService( permission_repo=permission_repo, space_repo=space_repo, inheritance_resolver=inheritance_resolver, ) @pytest.fixture def composable_service(space_repo, variable_repo, reference_repo, permission_repo): """Create a composable space service.""" return ComposableSpaceService( space_repo=space_repo, variable_repo=variable_repo, reference_repo=reference_repo, permission_repo=permission_repo, ) # =========================================================================== # SpaceReference Model Tests # =========================================================================== class TestSpaceReferenceModel: """Tests for SpaceReference model.""" def test_create_reference(self): """Test creating a space reference.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.INCLUDES, ) assert ref.source_space_id == "space-1" assert ref.target_space_id == "space-2" assert ref.reference_type == SpaceReferenceType.INCLUDES assert ref.id is not None def test_reference_with_alias(self): """Test reference with alias.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.LINKS_TO, alias="common-components", ) assert ref.alias == "common-components" def test_reference_to_dict(self): """Test serialization to dict.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.EXTENDS, alias="base", metadata={"priority": 1}, ) data = ref.to_dict() assert data["source_space_id"] == "space-1" assert data["target_space_id"] == "space-2" assert data["reference_type"] == "extends" assert data["alias"] == "base" assert data["metadata"]["priority"] == 1 def test_reference_from_dict(self): """Test deserialization from dict.""" data = { "id": "ref-1", "source_space_id": "space-1", "target_space_id": "space-2", "reference_type": "composed_of", "alias": "part-a", "metadata": {"order": 1}, "created_at": "2024-01-01T00:00:00", } ref = SpaceReference.from_dict(data) assert ref.id == "ref-1" assert ref.reference_type == SpaceReferenceType.COMPOSED_OF assert ref.alias == "part-a" # =========================================================================== # SpacePermission Model Tests # =========================================================================== class TestSpacePermissionModel: """Tests for SpacePermission model.""" def test_create_permission(self): """Test creating a permission.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, ) assert perm.space_id == "space-1" assert perm.principal_id == "user-1" assert perm.access_level == AccessLevel.WRITE def test_permission_with_role(self): """Test permission with role.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, role=SpaceRole.OWNER, ) assert perm.role == SpaceRole.OWNER def test_permission_expiration(self): """Test permission expiration check.""" # Not expired perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, expires_at=datetime.now() + timedelta(days=1), ) assert not perm.is_expired() # Expired perm_expired = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-2", access_level=AccessLevel.READ, expires_at=datetime.now() - timedelta(days=1), ) assert perm_expired.is_expired() def test_has_access(self): """Test has_access check.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, ) assert perm.has_access(AccessLevel.READ) assert perm.has_access(AccessLevel.WRITE) assert not perm.has_access(AccessLevel.ADMIN) def test_permission_to_dict(self): """Test serialization.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, role=SpaceRole.OWNER, granted_by="admin", ) data = perm.to_dict() assert data["access_level"] == "admin" assert data["role"] == "owner" assert data["granted_by"] == "admin" # =========================================================================== # SpaceAccess Model Tests # =========================================================================== class TestSpaceAccessModel: """Tests for SpaceAccess model.""" def test_access_checks(self): """Test access level checks.""" access = SpaceAccess( space_id="space-1", principal_id="user-1", effective_level=AccessLevel.WRITE, ) assert access.can_read() assert access.can_write() assert not access.is_admin() def test_admin_access(self): """Test admin access.""" access = SpaceAccess( space_id="space-1", principal_id="user-1", effective_level=AccessLevel.ADMIN, roles=[SpaceRole.OWNER], ) assert access.can_read() assert access.can_write() assert access.is_admin() def test_no_access(self): """Test no access.""" access = SpaceAccess( space_id="space-1", principal_id="user-1", effective_level=AccessLevel.NONE, ) assert not access.can_read() assert not access.can_write() assert not access.is_admin() # =========================================================================== # InheritedVariable Tests # =========================================================================== class TestInheritedVariable: """Tests for InheritedVariable.""" def test_local_variable(self): """Test local variable detection.""" var = InheritedVariable( name="version", value="1.0", source_space_id="space-1", inheritance_depth=0, ) assert var.is_local() def test_inherited_variable(self): """Test inherited variable.""" var = InheritedVariable( name="company", value="Acme Corp", source_space_id="parent-space", inheritance_depth=2, ) assert not var.is_local() assert var.inheritance_depth == 2 # =========================================================================== # InheritedConfig Tests # =========================================================================== class TestInheritedConfig: """Tests for InheritedConfig.""" def test_default_config(self): """Test default config values.""" config = InheritedConfig() assert config.default_variant == "hierarchical" assert config.enable_caching is True assert config.theme is None def test_source_tracking(self): """Test config source tracking.""" config = InheritedConfig( theme="dark", source_spaces={"theme": "parent-space"}, ) assert config.get_source("theme") == "parent-space" assert config.is_inherited("theme", "child-space") assert not config.is_inherited("theme", "parent-space") # =========================================================================== # SpaceReferenceRepository Tests # =========================================================================== class TestSqliteSpaceReferenceRepository: """Tests for SQLite space reference repository.""" def test_add_reference(self, reference_repo): """Test adding a reference.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.INCLUDES, ) saved = reference_repo.add_reference(ref) assert saved.id == ref.id def test_get_reference(self, reference_repo): """Test getting a reference by ID.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.LINKS_TO, ) reference_repo.add_reference(ref) found = reference_repo.get_reference(ref.id) assert found is not None assert found.source_space_id == "space-1" def test_get_references_from(self, reference_repo): """Test getting references from a source.""" ref1 = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.INCLUDES, ) ref2 = SpaceReference( source_space_id="space-1", target_space_id="space-3", reference_type=SpaceReferenceType.LINKS_TO, ) reference_repo.add_reference(ref1) reference_repo.add_reference(ref2) refs = reference_repo.get_references_from("space-1") assert len(refs) == 2 # Filter by type refs_includes = reference_repo.get_references_from( "space-1", SpaceReferenceType.INCLUDES ) assert len(refs_includes) == 1 assert refs_includes[0].target_space_id == "space-2" def test_get_references_to(self, reference_repo): """Test getting references to a target.""" ref1 = SpaceReference( source_space_id="space-1", target_space_id="space-3", reference_type=SpaceReferenceType.INCLUDES, ) ref2 = SpaceReference( source_space_id="space-2", target_space_id="space-3", reference_type=SpaceReferenceType.LINKS_TO, ) reference_repo.add_reference(ref1) reference_repo.add_reference(ref2) refs = reference_repo.get_references_to("space-3") assert len(refs) == 2 def test_remove_reference(self, reference_repo): """Test removing a reference.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.INCLUDES, ) reference_repo.add_reference(ref) assert reference_repo.remove_reference(ref.id) assert reference_repo.get_reference(ref.id) is None def test_reference_exists(self, reference_repo): """Test checking if reference exists.""" ref = SpaceReference( source_space_id="space-1", target_space_id="space-2", reference_type=SpaceReferenceType.EXTENDS, ) reference_repo.add_reference(ref) assert reference_repo.reference_exists( "space-1", "space-2", SpaceReferenceType.EXTENDS ) assert not reference_repo.reference_exists( "space-1", "space-2", SpaceReferenceType.INCLUDES ) # =========================================================================== # AccessControlRepository Tests # =========================================================================== class TestSqliteAccessControlRepository: """Tests for SQLite access control repository.""" def test_grant_permission(self, permission_repo): """Test granting a permission.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, ) saved = permission_repo.grant_permission(perm) assert saved.space_id == "space-1" def test_get_permission(self, permission_repo): """Test getting a specific permission.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) permission_repo.grant_permission(perm) found = permission_repo.get_permission("space-1", "user", "user-1") assert found is not None assert found.access_level == AccessLevel.READ def test_revoke_permission(self, permission_repo): """Test revoking a permission.""" perm = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, ) permission_repo.grant_permission(perm) assert permission_repo.revoke_permission("space-1", "user", "user-1") assert permission_repo.get_permission("space-1", "user", "user-1") is None def test_get_permissions_for_space(self, permission_repo): """Test getting all permissions for a space.""" perm1 = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) perm2 = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-2", access_level=AccessLevel.WRITE, ) permission_repo.grant_permission(perm1) permission_repo.grant_permission(perm2) perms = permission_repo.get_permissions_for_space("space-1") assert len(perms) == 2 def test_get_permissions_for_principal(self, permission_repo): """Test getting all permissions for a principal.""" perm1 = SpacePermission( space_id="space-1", principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) perm2 = SpacePermission( space_id="space-2", principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, ) permission_repo.grant_permission(perm1) permission_repo.grant_permission(perm2) perms = permission_repo.get_permissions_for_principal("user", "user-1") assert len(perms) == 2 # =========================================================================== # InheritanceResolver Tests # =========================================================================== class TestInheritanceResolver: """Tests for inheritance resolution.""" def test_get_parent_chain_no_parent(self, inheritance_resolver, space_repo): """Test getting parent chain for space without parent.""" space = InformationSpace(name="root-space") space_repo.create(space) chain = inheritance_resolver.get_parent_chain(space.id) assert chain == [] def test_get_parent_chain_single_parent(self, inheritance_resolver, space_repo): """Test getting parent chain with one parent.""" parent = InformationSpace(name="parent-space") space_repo.create(parent) child = InformationSpace(name="child-space", parent_space_id=parent.id) space_repo.create(child) chain = inheritance_resolver.get_parent_chain(child.id) assert len(chain) == 1 assert chain[0].id == parent.id def test_get_parent_chain_multiple_levels(self, inheritance_resolver, space_repo): """Test getting parent chain with multiple levels.""" grandparent = InformationSpace(name="grandparent") space_repo.create(grandparent) parent = InformationSpace(name="parent", parent_space_id=grandparent.id) space_repo.create(parent) child = InformationSpace(name="child", parent_space_id=parent.id) space_repo.create(child) chain = inheritance_resolver.get_parent_chain(child.id) assert len(chain) == 2 assert chain[0].id == parent.id assert chain[1].id == grandparent.id def test_resolve_variable_local(self, inheritance_resolver, space_repo, variable_repo): """Test resolving a local variable.""" space = InformationSpace(name="test-space") space_repo.create(space) var = SpaceVariable(space_id=space.id, name="version", value="1.0") variable_repo.set_variable(var) resolved = inheritance_resolver.resolve_variable(space.id, "version") assert resolved is not None assert resolved.value == "1.0" assert resolved.is_local() def test_resolve_variable_inherited( self, inheritance_resolver, space_repo, variable_repo ): """Test resolving an inherited variable.""" parent = InformationSpace(name="parent-space") space_repo.create(parent) child = InformationSpace(name="child-space", parent_space_id=parent.id) space_repo.create(child) # Set variable in parent only var = SpaceVariable(space_id=parent.id, name="company", value="Acme") variable_repo.set_variable(var) resolved = inheritance_resolver.resolve_variable(child.id, "company") assert resolved is not None assert resolved.value == "Acme" assert not resolved.is_local() assert resolved.source_space_id == parent.id def test_resolve_variable_override( self, inheritance_resolver, space_repo, variable_repo ): """Test that local variable overrides inherited.""" parent = InformationSpace(name="parent-space") space_repo.create(parent) child = InformationSpace(name="child-space", parent_space_id=parent.id) space_repo.create(child) # Set in both var_parent = SpaceVariable(space_id=parent.id, name="version", value="1.0") variable_repo.set_variable(var_parent) var_child = SpaceVariable(space_id=child.id, name="version", value="2.0") variable_repo.set_variable(var_child) resolved = inheritance_resolver.resolve_variable(child.id, "version") assert resolved is not None assert resolved.value == "2.0" assert resolved.is_local() def test_resolve_all_variables( self, inheritance_resolver, space_repo, variable_repo ): """Test resolving all variables including inherited.""" parent = InformationSpace(name="parent") space_repo.create(parent) child = InformationSpace(name="child", parent_space_id=parent.id) space_repo.create(child) # Parent variables variable_repo.set_variable( SpaceVariable(space_id=parent.id, name="company", value="Acme") ) variable_repo.set_variable( SpaceVariable(space_id=parent.id, name="version", value="1.0") ) # Child variables (one override, one new) variable_repo.set_variable( SpaceVariable(space_id=child.id, name="version", value="2.0") ) variable_repo.set_variable( SpaceVariable(space_id=child.id, name="author", value="John") ) all_vars = inheritance_resolver.resolve_all_variables(child.id) assert len(all_vars) == 3 assert all_vars["company"].value == "Acme" assert not all_vars["company"].is_local() assert all_vars["version"].value == "2.0" assert all_vars["version"].is_local() assert all_vars["author"].value == "John" def test_resolve_config_inheritance(self, inheritance_resolver, space_repo): """Test resolving config with inheritance.""" parent = InformationSpace( name="parent", config=SpaceConfig(theme="dark", enable_caching=True), ) space_repo.create(parent) child = InformationSpace( name="child", parent_space_id=parent.id, config=SpaceConfig(default_variant="flat"), ) space_repo.create(child) config = inheritance_resolver.resolve_config(child.id) assert config.theme == "dark" # inherited assert config.default_variant == "flat" # local assert config.source_spaces["theme"] == parent.id # =========================================================================== # AccessControlService Tests # =========================================================================== class TestAccessControlService: """Tests for access control service.""" def test_grant_permission(self, access_service, space_repo): """Test granting a permission.""" space = InformationSpace(name="test-space") space_repo.create(space) perm = access_service.grant_permission( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, ) assert perm.access_level == AccessLevel.WRITE def test_check_access(self, access_service, space_repo): """Test checking access.""" space = InformationSpace(name="test-space") space_repo.create(space) access_service.grant_permission( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, ) assert access_service.check_access( space.id, "user", "user-1", AccessLevel.READ ) assert access_service.check_access( space.id, "user", "user-1", AccessLevel.WRITE ) assert not access_service.check_access( space.id, "user", "user-1", AccessLevel.ADMIN ) def test_revoke_permission(self, access_service, space_repo): """Test revoking access.""" space = InformationSpace(name="test-space") space_repo.create(space) access_service.grant_permission( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) assert access_service.revoke_permission(space.id, "user", "user-1") assert not access_service.check_access( space.id, "user", "user-1", AccessLevel.READ ) def test_effective_access_direct(self, access_service, space_repo): """Test getting effective access with direct permission.""" space = InformationSpace(name="test-space") space_repo.create(space) access_service.grant_permission( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, role=SpaceRole.OWNER, ) access = access_service.get_effective_access(space.id, "user", "user-1") assert access.effective_level == AccessLevel.ADMIN assert SpaceRole.OWNER in access.roles def test_effective_access_inherited(self, access_service, space_repo): """Test getting effective access with inherited permission.""" parent = InformationSpace(name="parent") space_repo.create(parent) child = InformationSpace(name="child", parent_space_id=parent.id) space_repo.create(child) # Grant on parent only access_service.grant_permission( space_id=parent.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) access = access_service.get_effective_access(child.id, "user", "user-1") assert access.effective_level == AccessLevel.READ assert parent.id in access.inherited_from # =========================================================================== # ComposableSpaceService Tests # =========================================================================== class TestComposableSpaceService: """Tests for composable space service.""" def test_add_reference(self, composable_service, space_repo): """Test adding a space reference.""" space1 = InformationSpace(name="space-1") space2 = InformationSpace(name="space-2") space_repo.create(space1) space_repo.create(space2) ref = composable_service.add_reference( source_space_id=space1.id, target_space_id=space2.id, reference_type=SpaceReferenceType.INCLUDES, alias="shared", ) assert ref.alias == "shared" def test_add_reference_invalid_source(self, composable_service, space_repo): """Test adding reference with invalid source.""" space = InformationSpace(name="valid-space") space_repo.create(space) with pytest.raises(ValueError, match="Source space.*not found"): composable_service.add_reference( source_space_id="invalid-id", target_space_id=space.id, reference_type=SpaceReferenceType.LINKS_TO, ) def test_add_reference_self_reference(self, composable_service, space_repo): """Test that self-reference is rejected.""" space = InformationSpace(name="test-space") space_repo.create(space) with pytest.raises(ValueError, match="Cannot reference self"): composable_service.add_reference( source_space_id=space.id, target_space_id=space.id, reference_type=SpaceReferenceType.INCLUDES, ) def test_add_reference_circular_detection(self, composable_service, space_repo): """Test circular reference detection for EXTENDS type.""" space1 = InformationSpace(name="space-1") space2 = InformationSpace(name="space-2") space3 = InformationSpace(name="space-3") space_repo.create(space1) space_repo.create(space2) space_repo.create(space3) # space1 -> space2 -> space3 composable_service.add_reference( space1.id, space2.id, SpaceReferenceType.EXTENDS ) composable_service.add_reference( space2.id, space3.id, SpaceReferenceType.EXTENDS ) # space3 -> space1 would create a cycle with pytest.raises(CircularReferenceError): composable_service.add_reference( space3.id, space1.id, SpaceReferenceType.EXTENDS ) def test_remove_reference(self, composable_service, space_repo): """Test removing a reference.""" space1 = InformationSpace(name="space-1") space2 = InformationSpace(name="space-2") space_repo.create(space1) space_repo.create(space2) ref = composable_service.add_reference( space1.id, space2.id, SpaceReferenceType.LINKS_TO ) assert composable_service.remove_reference(ref.id) def test_get_references_from_and_to(self, composable_service, space_repo): """Test getting references.""" space1 = InformationSpace(name="space-1") space2 = InformationSpace(name="space-2") space3 = InformationSpace(name="space-3") space_repo.create(space1) space_repo.create(space2) space_repo.create(space3) composable_service.add_reference( space1.id, space2.id, SpaceReferenceType.INCLUDES ) composable_service.add_reference( space1.id, space3.id, SpaceReferenceType.LINKS_TO ) refs_from = composable_service.get_references_from(space1.id) assert len(refs_from) == 2 refs_to = composable_service.get_references_to(space2.id) assert len(refs_to) == 1 def test_get_composed_spaces(self, composable_service, space_repo): """Test getting all composed spaces.""" parent = InformationSpace(name="parent") space_repo.create(parent) main = InformationSpace(name="main", parent_space_id=parent.id) space_repo.create(main) included = InformationSpace(name="included") extended = InformationSpace(name="extended") space_repo.create(included) space_repo.create(extended) composable_service.add_reference( main.id, included.id, SpaceReferenceType.INCLUDES ) composable_service.add_reference( main.id, extended.id, SpaceReferenceType.EXTENDS ) composed = composable_service.get_composed_spaces(main.id) assert len(composed) == 3 # parent, included, extended ids = {s.id for s in composed} assert parent.id in ids assert included.id in ids assert extended.id in ids def test_resolve_variable(self, composable_service, space_repo, variable_repo): """Test variable resolution via service.""" parent = InformationSpace(name="parent") space_repo.create(parent) child = InformationSpace(name="child", parent_space_id=parent.id) space_repo.create(child) variable_repo.set_variable( SpaceVariable(space_id=parent.id, name="inherited_var", value="from parent") ) resolved = composable_service.resolve_variable(child.id, "inherited_var") assert resolved is not None assert resolved.value == "from parent" def test_resolve_config(self, composable_service, space_repo): """Test config resolution via service.""" space = InformationSpace( name="test-space", config=SpaceConfig(theme="minimal") ) space_repo.create(space) config = composable_service.resolve_config(space.id) assert config.theme == "minimal" def test_grant_and_check_access(self, composable_service, space_repo): """Test access control via service.""" space = InformationSpace(name="test-space") space_repo.create(space) composable_service.grant_access( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.WRITE, role=SpaceRole.EDITOR, ) assert composable_service.check_access( space.id, "user", "user-1", AccessLevel.WRITE ) assert not composable_service.check_access( space.id, "user", "user-1", AccessLevel.ADMIN ) def test_revoke_access(self, composable_service, space_repo): """Test revoking access via service.""" space = InformationSpace(name="test-space") space_repo.create(space) composable_service.grant_access( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.READ, ) assert composable_service.revoke_access(space.id, "user", "user-1") assert not composable_service.check_access( space.id, "user", "user-1", AccessLevel.READ ) def test_get_parent_chain(self, composable_service, space_repo): """Test getting parent chain via service.""" grandparent = InformationSpace(name="grandparent") space_repo.create(grandparent) parent = InformationSpace(name="parent", parent_space_id=grandparent.id) space_repo.create(parent) child = InformationSpace(name="child", parent_space_id=parent.id) space_repo.create(child) chain = composable_service.get_parent_chain(child.id) assert len(chain) == 2 assert chain[0].name == "parent" assert chain[1].name == "grandparent" def test_get_space_permissions(self, composable_service, space_repo): """Test getting space permissions.""" space = InformationSpace(name="test-space") space_repo.create(space) composable_service.grant_access( space.id, "user", "user-1", AccessLevel.READ ) composable_service.grant_access( space.id, "user", "user-2", AccessLevel.WRITE ) perms = composable_service.get_space_permissions(space.id) assert len(perms) == 2 # =========================================================================== # Edge Cases and Error Handling # =========================================================================== class TestEdgeCases: """Tests for edge cases and error handling.""" def test_max_inheritance_depth(self, space_repo, variable_repo): """Test max inheritance depth is enforced.""" resolver = InheritanceResolver( space_repo=space_repo, variable_repo=variable_repo, max_depth=3, ) # Create a chain of 5 spaces spaces = [] for i in range(5): parent_id = spaces[-1].id if spaces else None space = InformationSpace(name=f"space-{i}", parent_space_id=parent_id) space_repo.create(space) spaces.append(space) # Getting parent chain from the deepest should raise error with pytest.raises(CircularReferenceError, match="Maximum inheritance depth"): resolver.get_parent_chain(spaces[-1].id) def test_permission_expiration_handling(self, access_service, space_repo): """Test that expired permissions are not counted.""" space = InformationSpace(name="test-space") space_repo.create(space) # Grant expired permission directly in repo expired_perm = SpacePermission( space_id=space.id, principal_type="user", principal_id="user-1", access_level=AccessLevel.ADMIN, expires_at=datetime.now() - timedelta(days=1), ) access_service._permission_repo.grant_permission(expired_perm) # Should not have access access = access_service.get_effective_access(space.id, "user", "user-1") assert access.effective_level == AccessLevel.NONE def test_nonexistent_variable(self, inheritance_resolver, space_repo): """Test resolving nonexistent variable returns None.""" space = InformationSpace(name="test-space") space_repo.create(space) resolved = inheritance_resolver.resolve_variable(space.id, "nonexistent") assert resolved is None def test_reference_type_filtering(self, composable_service, space_repo): """Test filtering references by type.""" space1 = InformationSpace(name="space-1") space2 = InformationSpace(name="space-2") space3 = InformationSpace(name="space-3") space_repo.create(space1) space_repo.create(space2) space_repo.create(space3) composable_service.add_reference( space1.id, space2.id, SpaceReferenceType.INCLUDES ) composable_service.add_reference( space1.id, space3.id, SpaceReferenceType.EXTENDS ) # Filter by type includes = composable_service.get_references_from( space1.id, SpaceReferenceType.INCLUDES ) assert len(includes) == 1 assert includes[0].target_space_id == space2.id extends = composable_service.get_references_from( space1.id, SpaceReferenceType.EXTENDS ) assert len(extends) == 1 assert extends[0].target_space_id == space3.id