Files
markitect-main/tests/unit/spaces/test_api.py
tegwick 7de57a389d feat(spaces): implement Phase 6 API Layer
Implements API layer for Information Spaces:
- GraphQL schema types for spaces, documents, variables
- GraphQL queries and mutations for space operations
- CLI command group with all space management commands
- Resolver functions connecting GraphQL to SpaceService
- 38 unit tests for API components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 12:29:11 +01:00

538 lines
17 KiB
Python

"""
Unit tests for Phase 6: API Layer components.
Tests cover:
- GraphQL space schema types
- CLI space commands
"""
import pytest
import tempfile
import json
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch
from click.testing import CliRunner
from markitect.graphql.space_schema import (
InformationSpaceType,
SpaceDocumentType,
SpaceVariableType,
SpaceConfigType,
SpaceMetadataType,
RenderResultType,
SyncResultType,
SpaceQuery,
SpaceMutation,
CreateSpaceInput,
UpdateSpaceInput,
AddDocumentInput,
CreateSpacePayload,
UpdateSpacePayload,
DeleteSpacePayload,
AddDocumentPayload,
SpaceStatusEnum,
SyncDirectionEnum,
ConflictResolutionEnum,
)
from markitect.spaces.cli import (
space,
create_space,
list_spaces,
show_space,
delete_space,
add_document,
list_documents,
set_variable,
list_variables,
export_space,
sync_space,
)
from markitect.spaces.models import (
InformationSpace,
SpaceDocument,
SpaceVariable,
SpaceConfig,
SpaceMetadata,
SpaceStatus,
)
class TestSpaceStatusEnum:
"""Tests for SpaceStatusEnum."""
def test_status_values(self):
"""Test all status values are defined."""
assert SpaceStatusEnum.DRAFT
assert SpaceStatusEnum.ACTIVE
assert SpaceStatusEnum.ARCHIVED
assert SpaceStatusEnum.DELETED
class TestSyncDirectionEnum:
"""Tests for SyncDirectionEnum."""
def test_direction_values(self):
"""Test all direction values are defined."""
assert SyncDirectionEnum.SPACE_TO_DIRECTORY
assert SyncDirectionEnum.DIRECTORY_TO_SPACE
assert SyncDirectionEnum.BIDIRECTIONAL
class TestConflictResolutionEnum:
"""Tests for ConflictResolutionEnum."""
def test_resolution_values(self):
"""Test all resolution values are defined."""
assert ConflictResolutionEnum.SPACE_WINS
assert ConflictResolutionEnum.DIRECTORY_WINS
assert ConflictResolutionEnum.NEWER_WINS
assert ConflictResolutionEnum.SKIP
class TestInformationSpaceType:
"""Tests for InformationSpaceType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
# Check field definitions exist on the type
assert hasattr(InformationSpaceType, 'id')
assert hasattr(InformationSpaceType, 'name')
assert hasattr(InformationSpaceType, 'description')
assert hasattr(InformationSpaceType, 'status')
assert hasattr(InformationSpaceType, 'config')
assert hasattr(InformationSpaceType, 'metadata')
class TestSpaceDocumentType:
"""Tests for SpaceDocumentType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
assert hasattr(SpaceDocumentType, 'id')
assert hasattr(SpaceDocumentType, 'space_id')
assert hasattr(SpaceDocumentType, 'document_id')
assert hasattr(SpaceDocumentType, 'space_path')
class TestSpaceVariableType:
"""Tests for SpaceVariableType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
assert hasattr(SpaceVariableType, 'space_id')
assert hasattr(SpaceVariableType, 'name')
assert hasattr(SpaceVariableType, 'value')
assert hasattr(SpaceVariableType, 'scope')
class TestSpaceConfigType:
"""Tests for SpaceConfigType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
assert hasattr(SpaceConfigType, 'default_variant')
assert hasattr(SpaceConfigType, 'enable_caching')
assert hasattr(SpaceConfigType, 'theme')
class TestRenderResultType:
"""Tests for RenderResultType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
assert hasattr(RenderResultType, 'content')
assert hasattr(RenderResultType, 'format')
assert hasattr(RenderResultType, 'content_hash')
class TestSyncResultType:
"""Tests for SyncResultType GraphQL type."""
def test_type_has_required_fields(self):
"""Test that type has required fields."""
assert hasattr(SyncResultType, 'success')
assert hasattr(SyncResultType, 'created_count')
assert hasattr(SyncResultType, 'updated_count')
assert hasattr(SyncResultType, 'conflict_count')
class TestSpaceQuery:
"""Tests for SpaceQuery GraphQL type."""
def test_query_has_space_field(self):
"""Test that query has space field."""
assert hasattr(SpaceQuery, 'space')
def test_query_has_spaces_field(self):
"""Test that query has spaces field."""
assert hasattr(SpaceQuery, 'spaces')
def test_query_has_document_fields(self):
"""Test that query has document fields."""
assert hasattr(SpaceQuery, 'space_documents')
assert hasattr(SpaceQuery, 'space_document')
def test_query_has_variable_fields(self):
"""Test that query has variable fields."""
assert hasattr(SpaceQuery, 'space_variables')
assert hasattr(SpaceQuery, 'space_variable')
class TestSpaceMutation:
"""Tests for SpaceMutation GraphQL type."""
def test_mutation_has_space_lifecycle(self):
"""Test mutation has space lifecycle operations."""
assert hasattr(SpaceMutation, 'create_space')
assert hasattr(SpaceMutation, 'update_space')
assert hasattr(SpaceMutation, 'delete_space')
assert hasattr(SpaceMutation, 'activate_space')
assert hasattr(SpaceMutation, 'archive_space')
def test_mutation_has_document_operations(self):
"""Test mutation has document operations."""
assert hasattr(SpaceMutation, 'add_document')
assert hasattr(SpaceMutation, 'remove_document')
def test_mutation_has_variable_operations(self):
"""Test mutation has variable operations."""
assert hasattr(SpaceMutation, 'set_variable')
assert hasattr(SpaceMutation, 'delete_variable')
def test_mutation_has_render_operations(self):
"""Test mutation has render operations."""
assert hasattr(SpaceMutation, 'render_document')
assert hasattr(SpaceMutation, 'render_space')
def test_mutation_has_sync_operations(self):
"""Test mutation has sync operations."""
assert hasattr(SpaceMutation, 'sync_space')
assert hasattr(SpaceMutation, 'export_space')
assert hasattr(SpaceMutation, 'import_directory')
class TestPayloadTypes:
"""Tests for payload types."""
def test_create_space_payload(self):
"""Test CreateSpacePayload type."""
assert hasattr(CreateSpacePayload, 'space')
assert hasattr(CreateSpacePayload, 'success')
assert hasattr(CreateSpacePayload, 'errors')
def test_update_space_payload(self):
"""Test UpdateSpacePayload type."""
assert hasattr(UpdateSpacePayload, 'space')
assert hasattr(UpdateSpacePayload, 'success')
assert hasattr(UpdateSpacePayload, 'errors')
def test_delete_space_payload(self):
"""Test DeleteSpacePayload type."""
assert hasattr(DeleteSpacePayload, 'success')
assert hasattr(DeleteSpacePayload, 'deleted_id')
assert hasattr(DeleteSpacePayload, 'errors')
def test_add_document_payload(self):
"""Test AddDocumentPayload type."""
assert hasattr(AddDocumentPayload, 'document')
assert hasattr(AddDocumentPayload, 'success')
assert hasattr(AddDocumentPayload, 'errors')
# CLI Tests
class TestCLISpaceGroup:
"""Tests for CLI space command group."""
def test_space_group_exists(self):
"""Test space command group exists."""
assert space is not None
assert hasattr(space, 'commands')
def test_space_commands_registered(self):
"""Test that expected commands are registered."""
commands = space.commands
expected = [
'create', 'list', 'show', 'delete',
'activate', 'archive',
'add-doc', 'remove-doc', 'list-docs',
'set-var', 'get-var', 'list-vars',
'render', 'export', 'import', 'sync',
'invalidate-cache',
]
for cmd in expected:
assert cmd in commands, f"Command '{cmd}' not found in space group"
class TestCLICreateSpace:
"""Tests for CLI create space command."""
@patch('markitect.spaces.cli.get_space_service')
def test_create_space_basic(self, mock_get_service):
"""Test basic space creation."""
mock_service = Mock()
mock_space = InformationSpace(
id="test-id",
name="Test Space",
status=SpaceStatus.DRAFT,
)
mock_service.create_space.return_value = mock_space
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['create', 'Test Space'])
assert result.exit_code == 0
assert 'Created space' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_create_space_with_options(self, mock_get_service):
"""Test space creation with options."""
mock_service = Mock()
mock_space = InformationSpace(
id="test-id",
name="Test Space",
description="A test",
status=SpaceStatus.DRAFT,
)
mock_service.create_space.return_value = mock_space
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, [
'create', 'Test Space',
'--description', 'A test',
'--theme', 'github',
])
assert result.exit_code == 0
mock_service.create_space.assert_called_once()
@patch('markitect.spaces.cli.get_space_service')
def test_create_space_json_output(self, mock_get_service):
"""Test space creation with JSON output."""
from datetime import datetime
mock_service = Mock()
mock_space = InformationSpace(
id="test-id",
name="Test Space",
status=SpaceStatus.DRAFT,
created_at=datetime.now(),
)
mock_service.create_space.return_value = mock_space
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['create', 'Test Space', '-j'])
assert result.exit_code == 0
data = json.loads(result.output)
assert data['id'] == 'test-id'
assert data['name'] == 'Test Space'
class TestCLIListSpaces:
"""Tests for CLI list spaces command."""
@patch('markitect.spaces.cli.get_space_service')
def test_list_spaces_empty(self, mock_get_service):
"""Test listing empty spaces."""
mock_service = Mock()
mock_service.list_spaces.return_value = []
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['list'])
assert result.exit_code == 0
assert 'No spaces found' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_list_spaces_with_results(self, mock_get_service):
"""Test listing spaces with results."""
mock_service = Mock()
mock_service.list_spaces.return_value = [
InformationSpace(id="id-1", name="Space 1", status=SpaceStatus.ACTIVE),
InformationSpace(id="id-2", name="Space 2", status=SpaceStatus.DRAFT),
]
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['list'])
assert result.exit_code == 0
assert 'Space 1' in result.output
assert 'Space 2' in result.output
class TestCLIShowSpace:
"""Tests for CLI show space command."""
@patch('markitect.spaces.cli.get_space_service')
def test_show_space_not_found(self, mock_get_service):
"""Test showing non-existent space."""
mock_service = Mock()
mock_service.get_space.return_value = None
mock_service.get_space_by_name.return_value = None
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['show', 'nonexistent'])
assert result.exit_code == 1
assert 'not found' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_show_space_found(self, mock_get_service):
"""Test showing existing space."""
mock_service = Mock()
mock_space = InformationSpace(
id="test-id",
name="Test Space",
description="A test space",
status=SpaceStatus.ACTIVE,
config=SpaceConfig(),
)
mock_service.get_space.return_value = mock_space
mock_service.list_documents.return_value = []
mock_service.list_variables.return_value = []
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['show', 'test-id'])
assert result.exit_code == 0
assert 'Test Space' in result.output
assert 'active' in result.output
class TestCLIDeleteSpace:
"""Tests for CLI delete space command."""
@patch('markitect.spaces.cli.get_space_service')
def test_delete_space_with_force(self, mock_get_service):
"""Test deleting space with force flag."""
mock_service = Mock()
mock_service.get_space.return_value = InformationSpace(
id="test-id",
name="Test Space",
status=SpaceStatus.ACTIVE,
)
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['delete', 'test-id', '-f'])
assert result.exit_code == 0
assert 'Deleted space' in result.output
mock_service.delete_space.assert_called_once_with('test-id')
class TestCLIDocumentCommands:
"""Tests for CLI document commands."""
@patch('markitect.spaces.cli.get_space_service')
def test_add_document(self, mock_get_service):
"""Test adding a document."""
mock_service = Mock()
mock_doc = SpaceDocument(
id="doc-id",
space_id="space-id",
document_id="doc-id",
space_path="/test.md",
)
mock_service.add_document.return_value = mock_doc
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, [
'add-doc', 'space-id',
'--path', '/test.md',
'--document-id', 'doc-id',
])
assert result.exit_code == 0
assert 'Added document' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_list_documents_empty(self, mock_get_service):
"""Test listing empty documents."""
mock_service = Mock()
mock_service.list_documents.return_value = []
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['list-docs', 'space-id'])
assert result.exit_code == 0
assert 'No documents' in result.output
class TestCLIVariableCommands:
"""Tests for CLI variable commands."""
@patch('markitect.spaces.cli.get_space_service')
def test_set_variable(self, mock_get_service):
"""Test setting a variable."""
mock_service = Mock()
mock_var = SpaceVariable(
space_id="space-id",
name="test_var",
value="test_value",
scope="space",
)
mock_service.set_variable.return_value = mock_var
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, [
'set-var', 'space-id', 'test_var', 'test_value',
])
assert result.exit_code == 0
assert 'Set variable' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_list_variables_empty(self, mock_get_service):
"""Test listing empty variables."""
mock_service = Mock()
mock_service.list_variables.return_value = []
mock_get_service.return_value = mock_service
runner = CliRunner()
result = runner.invoke(space, ['list-vars', 'space-id'])
assert result.exit_code == 0
assert 'No variables' in result.output
class TestCLISyncCommands:
"""Tests for CLI sync commands."""
@patch('markitect.spaces.cli.get_space_service')
def test_export_space_not_found(self, mock_get_service):
"""Test exporting non-existent space."""
mock_service = Mock()
mock_service.get_space.return_value = None
mock_get_service.return_value = mock_service
runner = CliRunner()
with tempfile.TemporaryDirectory() as tmpdir:
result = runner.invoke(space, ['export', 'nonexistent', tmpdir])
assert result.exit_code == 1
assert 'not found' in result.output
@patch('markitect.spaces.cli.get_space_service')
def test_sync_space_not_found(self, mock_get_service):
"""Test syncing non-existent space."""
mock_service = Mock()
mock_service.get_space.return_value = None
mock_get_service.return_value = mock_service
runner = CliRunner()
with tempfile.TemporaryDirectory() as tmpdir:
result = runner.invoke(space, ['sync', 'nonexistent', tmpdir])
assert result.exit_code == 1
assert 'not found' in result.output