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>
538 lines
17 KiB
Python
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
|