feat: reorganize tests by capability with separate test targets
Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled

Separate capability-specific tests from core system tests to establish clear
test organization and separation of concerns.

## Test Reorganization:
- **markitect-content tests**: Moved 6 tests to capabilities/markitect-content/tests/
- **markitect-finance tests**: Moved 7 tests to markitect/finance/tests/
- **markitect-query tests**: Moved 1 test to markitect/query_paradigms/tests/
- **markitect-graphql tests**: Moved 2 tests to markitect/graphql/tests/
- **markitect-plugins tests**: Moved 2 tests to markitect/plugins/tests/

## Makefile Updates:
- **make test**: Excludes capability tests, runs only core system tests
- **make test-capabilities**: Runs all capability tests
- **make test-capability-***: Individual capability test targets
- Updated all test targets (test-red, test-green, test-ultra-fast, test-perf)
- Added capability test targets to help documentation

## Benefits:
- Clear separation between core system tests and capability-specific tests
- Faster core test execution (capability tests not run by default)
- Individual capability testing for focused development
- Supports future capability extraction workflow
- Maintains capability test independence

Test verification:
- Core tests: 1291 tests (capability tests excluded)
- Finance capability: 143 tests working independently
- Content capability: 79 tests working independently

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-25 02:37:45 +02:00
parent f0dfd04d45
commit 096017b93f
23 changed files with 74 additions and 8 deletions

View File

View File

@@ -0,0 +1,797 @@
"""
Comprehensive tests for GraphQL mutations (Issue #10).
Tests all aspects of the GraphQL write interface including:
- Mutation schema validation
- Markdown file CRUD operations
- Schema CRUD operations
- Error handling
- CLI integration
"""
import pytest
import json
import sqlite3
import tempfile
import os
from datetime import datetime
from pathlib import Path
from unittest.mock import Mock, patch
from markitect.graphql.schema import schema
from markitect.database import DatabaseManager
@pytest.fixture
def temp_db_path():
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize database with test data
db_manager = DatabaseManager(db_path)
db_manager.initialize_database()
yield db_path
# Cleanup
os.unlink(db_path)
@pytest.fixture
def populated_db_path():
"""Create temporary database with some test data."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize database with test data
db_manager = DatabaseManager(db_path)
db_manager.initialize_database()
# Add sample data
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Sample markdown file
cursor.execute("""
INSERT INTO markdown_files (filename, content, front_matter, created_at)
VALUES (?, ?, ?, ?)
""", (
'existing.md',
'# Existing Document\n\nThis document already exists.',
'{"title": "Existing Document"}',
datetime.now().isoformat()
))
# Sample schema
cursor.execute("""
INSERT INTO schemas (filename, title, description, schema_content, created_at)
VALUES (?, ?, ?, ?, ?)
""", (
'existing-schema.json',
'Existing Schema',
'A schema that already exists',
'{"type": "object", "properties": {"name": {"type": "string"}}}',
datetime.now().isoformat()
))
conn.commit()
conn.close()
yield db_path
# Cleanup
os.unlink(db_path)
class TestGraphQLMutationSchema:
"""Test GraphQL mutation schema definition and validation."""
def test_schema_has_mutations(self):
"""Test that the GraphQL schema has mutations."""
result = schema.execute('''
{
__schema {
mutationType {
name
fields {
name
description
}
}
}
}
''')
assert result.errors is None
mutation_type = result.data['__schema']['mutationType']
assert mutation_type is not None
assert mutation_type['name'] == 'Mutation'
field_names = [field['name'] for field in mutation_type['fields']]
assert 'addMarkdownFile' in field_names
assert 'updateMarkdownFile' in field_names
assert 'addSchema' in field_names
assert 'updateSchema' in field_names
assert 'deleteSchema' in field_names
def test_add_markdown_file_mutation_signature(self):
"""Test addMarkdownFile mutation has correct signature."""
result = schema.execute('''
{
__schema {
mutationType {
fields {
name
args {
name
type {
name
}
}
}
}
}
}
''')
mutation_fields = result.data['__schema']['mutationType']['fields']
add_file_field = next(f for f in mutation_fields if f['name'] == 'addMarkdownFile')
arg_names = [arg['name'] for arg in add_file_field['args']]
assert 'filename' in arg_names
assert 'content' in arg_names
def test_mutation_payload_types(self):
"""Test that mutation payload types have correct structure."""
result = schema.execute('''
{
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
''')
types = {t['name']: t for t in result.data['__schema']['types']}
# Check AddMarkdownFilePayload
payload = types.get('AddMarkdownFilePayload')
assert payload is not None
field_names = [f['name'] for f in payload['fields']]
assert 'markdownFile' in field_names
assert 'success' in field_names
assert 'errors' in field_names
class TestMarkdownFileMutations:
"""Test markdown file CRUD mutations."""
def test_add_markdown_file_success(self, temp_db_path):
"""Test successful markdown file creation."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
addMarkdownFile(
filename: "new-file.md"
content: "# New File\\n\\nThis is new content."
) {
success
markdownFile {
id
filename
content
wordCount
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['addMarkdownFile']
assert data['success'] is True
assert len(data['errors']) == 0
assert data['markdownFile'] is not None
assert data['markdownFile']['filename'] == 'new-file.md'
assert 'New File' in data['markdownFile']['content']
assert data['markdownFile']['wordCount'] > 0
def test_add_markdown_file_with_front_matter(self, temp_db_path):
"""Test markdown file creation with front matter."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
content_with_frontmatter = '''---
title: "Test Document"
author: "Test Author"
tags: ["test", "markdown"]
---
# Test Document
This is a test document with front matter.
'''
mutation = '''
mutation {
addMarkdownFile(
filename: "with-frontmatter.md"
content: "%s"
) {
success
markdownFile {
id
filename
hasFrontMatter
frontMatter {
key
value
}
}
errors
}
}
''' % content_with_frontmatter.replace('\n', '\\n').replace('"', '\\"')
result = schema.execute(mutation)
assert result.errors is None
data = result.data['addMarkdownFile']
assert data['success'] is True
assert data['markdownFile']['hasFrontMatter'] is True
front_matter_keys = [fm['key'] for fm in data['markdownFile']['frontMatter']]
assert 'title' in front_matter_keys
assert 'author' in front_matter_keys
def test_add_markdown_file_duplicate_filename(self, populated_db_path):
"""Test adding a file with duplicate filename (should succeed as update)."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
mutation = '''
mutation {
addMarkdownFile(
filename: "existing.md"
content: "# Updated Content\\n\\nThis content replaces the existing."
) {
success
markdownFile {
filename
content
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['addMarkdownFile']
assert data['success'] is True
assert 'Updated Content' in data['markdownFile']['content']
def test_update_markdown_file_success(self, populated_db_path):
"""Test successful markdown file update."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
mutation = '''
mutation {
updateMarkdownFile(
id: 1
content: "# Updated Title\\n\\nThis content has been updated."
) {
success
markdownFile {
id
content
wordCount
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['updateMarkdownFile']
assert data['success'] is True
assert len(data['errors']) == 0
assert 'Updated Title' in data['markdownFile']['content']
def test_update_markdown_file_not_found(self, temp_db_path):
"""Test updating non-existent markdown file."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
updateMarkdownFile(
id: 999
content: "# This should fail"
) {
success
markdownFile {
id
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['updateMarkdownFile']
assert data['success'] is False
assert data['markdownFile'] is None
assert len(data['errors']) > 0
assert 'not found' in data['errors'][0].lower()
def test_update_markdown_file_no_content(self, populated_db_path):
"""Test updating markdown file without providing content."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
mutation = '''
mutation {
updateMarkdownFile(id: 1) {
success
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['updateMarkdownFile']
assert data['success'] is False
assert 'required' in data['errors'][0].lower()
class TestSchemaMutations:
"""Test JSON schema CRUD mutations."""
def test_add_schema_success(self, temp_db_path):
"""Test successful schema creation."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
schema_content = {
"type": "object",
"title": "User Schema",
"description": "Schema for user objects",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"required": ["name"]
}
mutation = '''
mutation {
addSchema(
filename: "user-schema.json"
schemaContent: "%s"
) {
success
schema {
id
filename
title
description
propertyCount
}
errors
}
}
''' % json.dumps(schema_content).replace('"', '\\"')
result = schema.execute(mutation)
assert result.errors is None
data = result.data['addSchema']
assert data['success'] is True
assert len(data['errors']) == 0
assert data['schema']['filename'] == 'user-schema.json'
assert data['schema']['title'] == 'User Schema'
assert data['schema']['propertyCount'] == 2
def test_add_schema_invalid_json(self, temp_db_path):
"""Test adding schema with invalid JSON."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
addSchema(
filename: "invalid-schema.json"
schemaContent: "{ invalid json }"
) {
success
schema {
id
}
errors
}
}
'''
result = schema.execute(mutation)
# GraphQL should reject invalid JSON at the schema validation level
assert result.errors is not None
assert len(result.errors) > 0
assert "Badly formed JSONString" in str(result.errors[0])
def test_update_schema_success(self, populated_db_path):
"""Test successful schema update."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
new_schema = {
"type": "object",
"title": "Updated Schema",
"properties": {
"name": {"type": "string"},
"email": {"type": "string", "format": "email"}
}
}
mutation = '''
mutation {
updateSchema(
id: 1
schemaContent: "%s"
) {
success
schema {
title
propertyCount
}
errors
}
}
''' % json.dumps(new_schema).replace('"', '\\"')
result = schema.execute(mutation)
assert result.errors is None
data = result.data['updateSchema']
assert data['success'] is True
assert data['schema']['title'] == 'Updated Schema'
assert data['schema']['propertyCount'] == 2
def test_update_schema_not_found(self, temp_db_path):
"""Test updating non-existent schema."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
updateSchema(
id: 999
schemaContent: "{\\"type\\": \\"object\\"}"
) {
success
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['updateSchema']
assert data['success'] is False
assert 'not found' in data['errors'][0].lower()
def test_delete_schema_success(self, populated_db_path):
"""Test successful schema deletion."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=populated_db_path):
mutation = '''
mutation {
deleteSchema(filename: "existing-schema.json") {
success
deletedFilename
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['deleteSchema']
assert data['success'] is True
assert data['deletedFilename'] == 'existing-schema.json'
assert len(data['errors']) == 0
def test_delete_schema_not_found(self, temp_db_path):
"""Test deleting non-existent schema."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
deleteSchema(filename: "nonexistent.json") {
success
deletedFilename
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['deleteSchema']
assert data['success'] is False
assert data['deletedFilename'] is None
class TestMutationErrorHandling:
"""Test error handling in mutations."""
def test_database_error_handling(self, temp_db_path):
"""Test mutation behavior when database is unavailable."""
# Use a non-existent database path
with patch('markitect.graphql.resolvers.get_default_database_path', return_value='/nonexistent/path.db'):
mutation = '''
mutation {
addMarkdownFile(
filename: "test.md"
content: "# Test"
) {
success
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
data = result.data['addMarkdownFile']
assert data['success'] is False
assert len(data['errors']) > 0
def test_invalid_mutation_syntax(self):
"""Test handling of invalid mutation syntax."""
mutation = '''
mutation {
addMarkdownFile(filename: "test.md") {
success
}
}
'''
result = schema.execute(mutation)
# Should have errors due to missing required 'content' argument
assert result.errors is not None
def test_missing_required_arguments(self):
"""Test mutations with missing required arguments."""
mutation = '''
mutation {
addSchema(filename: "test.json") {
success
errors
}
}
'''
result = schema.execute(mutation)
# Should have errors due to missing required 'schemaContent' argument
assert result.errors is not None
class TestMutationIntegration:
"""Test full integration of mutations with database."""
def test_crud_workflow(self, temp_db_path):
"""Test complete CRUD workflow for markdown files."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
# 1. Create a file
create_mutation = '''
mutation {
addMarkdownFile(
filename: "workflow-test.md"
content: "# Original Content\\n\\nOriginal text."
) {
success
markdownFile {
id
filename
content
}
}
}
'''
result = schema.execute(create_mutation)
assert result.data['addMarkdownFile']['success'] is True
file_id = result.data['addMarkdownFile']['markdownFile']['id']
# 2. Update the file
update_mutation = '''
mutation {
updateMarkdownFile(
id: %d
content: "# Updated Content\\n\\nUpdated text."
) {
success
markdownFile {
content
}
}
}
''' % file_id
result = schema.execute(update_mutation)
assert result.data['updateMarkdownFile']['success'] is True
assert 'Updated Content' in result.data['updateMarkdownFile']['markdownFile']['content']
def test_schema_crud_workflow(self, temp_db_path):
"""Test complete CRUD workflow for schemas."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
# 1. Create a schema
create_mutation = '''
mutation {
addSchema(
filename: "workflow-schema.json"
schemaContent: "{\\"type\\": \\"object\\", \\"title\\": \\"Original\\"}"
) {
success
schema {
id
title
}
}
}
'''
result = schema.execute(create_mutation)
assert result.data['addSchema']['success'] is True
schema_id = result.data['addSchema']['schema']['id']
# 2. Update the schema
update_mutation = '''
mutation {
updateSchema(
id: %d
schemaContent: "{\\"type\\": \\"object\\", \\"title\\": \\"Updated\\"}"
) {
success
schema {
title
}
}
}
''' % schema_id
result = schema.execute(update_mutation)
assert result.data['updateSchema']['success'] is True
assert result.data['updateSchema']['schema']['title'] == 'Updated'
# 3. Delete the schema
delete_mutation = '''
mutation {
deleteSchema(filename: "workflow-schema.json") {
success
deletedFilename
}
}
'''
result = schema.execute(delete_mutation)
assert result.data['deleteSchema']['success'] is True
assert result.data['deleteSchema']['deletedFilename'] == 'workflow-schema.json'
class TestMutationCLI:
"""Test CLI integration for mutations."""
def test_graphql_mutate_command_available(self):
"""Test that graphql-mutate command is available."""
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-mutate", "--help"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "Execute GraphQL mutations" in result.stdout
assert "--local" in result.stdout
assert "--variables" in result.stdout
def test_mutation_examples_in_help(self):
"""Test that mutation examples are included in help."""
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-examples"],
capture_output=True,
text=True
)
assert result.returncode == 0
assert "Mutation Examples" in result.stdout
assert "addMarkdownFile" in result.stdout
assert "updateMarkdownFile" in result.stdout
assert "addSchema" in result.stdout
assert "deleteSchema" in result.stdout
class TestMutationPayloads:
"""Test mutation payload structures."""
def test_add_markdown_file_payload_structure(self, temp_db_path):
"""Test AddMarkdownFilePayload has correct structure."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
mutation = '''
mutation {
addMarkdownFile(
filename: "payload-test.md"
content: "# Payload Test"
) {
success
markdownFile {
id
filename
content
wordCount
lineCount
hasFrontMatter
createdAt
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
payload = result.data['addMarkdownFile']
# Check payload structure
assert isinstance(payload['success'], bool)
assert isinstance(payload['errors'], list)
if payload['success']:
md_file = payload['markdownFile']
assert md_file is not None
assert isinstance(md_file['id'], int)
assert isinstance(md_file['filename'], str)
assert isinstance(md_file['wordCount'], int)
assert isinstance(md_file['lineCount'], int)
assert isinstance(md_file['hasFrontMatter'], bool)
def test_error_payload_structure(self, temp_db_path):
"""Test error payloads have correct structure."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value='/nonexistent/path.db'):
mutation = '''
mutation {
addMarkdownFile(
filename: "error-test.md"
content: "# Error Test"
) {
success
markdownFile {
id
}
errors
}
}
'''
result = schema.execute(mutation)
assert result.errors is None
payload = result.data['addMarkdownFile']
assert payload['success'] is False
assert payload['markdownFile'] is None
assert isinstance(payload['errors'], list)
assert len(payload['errors']) > 0
assert all(isinstance(error, str) for error in payload['errors'])

View File

@@ -0,0 +1,619 @@
"""
Comprehensive tests for GraphQL interface (Issue #9).
Tests all aspects of the GraphQL read interface including:
- Schema definition and validation
- Resolver functionality
- Server endpoints
- CLI integration
- Error handling
"""
import pytest
import json
import sqlite3
import tempfile
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import subprocess
import sys
import os
from datetime import datetime
from markitect.graphql.schema import schema, MarkdownFile, Schema as SchemaType, AST, DatabaseStats
from markitect.graphql.resolvers import Query, MarkiTectResolver, get_default_database_path
from markitect.graphql.server import GraphQLServer, GraphQLClient
from markitect.database import DatabaseManager
@pytest.fixture
def temp_db_path():
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize database with test data
db_manager = DatabaseManager(db_path)
db_manager.initialize_database()
# Add sample data
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Sample markdown file
cursor.execute("""
INSERT INTO markdown_files (filename, content, front_matter, created_at)
VALUES (?, ?, ?, ?)
""", (
'test.md',
'# Test Document\n\nThis is a test document with [a link](http://example.com).',
'{"title": "Test Document", "author": "Test Author"}',
datetime.now().isoformat()
))
# Sample schema
cursor.execute("""
INSERT INTO schemas (filename, title, description, schema_content, created_at)
VALUES (?, ?, ?, ?, ?)
""", (
'test-schema.json',
'Test Schema',
'A test schema for testing',
'{"type": "object", "properties": {"name": {"type": "string"}}}',
datetime.now().isoformat()
))
conn.commit()
conn.close()
yield db_path
# Cleanup
os.unlink(db_path)
@pytest.fixture
def graphql_resolver(temp_db_path):
"""Create GraphQL resolver with test database."""
return MarkiTectResolver(temp_db_path)
@pytest.fixture
def graphql_query(temp_db_path):
"""Create GraphQL Query instance with test database."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
return Query()
@pytest.fixture
def flask_app(temp_db_path):
"""Create Flask app for testing GraphQL server."""
server = GraphQLServer(db_path=temp_db_path, enable_cors=True)
app = server.create_app()
app.config['TESTING'] = True
return app
class TestGraphQLSchema:
"""Test GraphQL schema definition and validation."""
def test_schema_is_valid(self):
"""Test that the GraphQL schema is valid."""
assert schema is not None
assert hasattr(schema, 'execute')
def test_schema_has_required_types(self):
"""Test that schema contains all required types."""
schema_str = str(schema)
# Check for main types
assert 'MarkdownFile' in schema_str
assert 'Schema' in schema_str
assert 'AST' in schema_str
assert 'DatabaseStats' in schema_str
assert 'SearchResult' in schema_str
def test_query_type_fields(self):
"""Test that Query type has all required fields."""
schema_str = str(schema)
# Check for query fields
assert 'markdownFile' in schema_str
assert 'markdownFiles' in schema_str
assert 'schema' in schema_str
assert 'schemas' in schema_str
assert 'ast' in schema_str
assert 'search' in schema_str
assert 'databaseStats' in schema_str
assert 'astQuery' in schema_str
class TestGraphQLResolvers:
"""Test GraphQL resolver functionality."""
def test_resolver_initialization(self, temp_db_path):
"""Test resolver initializes correctly."""
resolver = MarkiTectResolver(temp_db_path)
assert resolver.db_path == temp_db_path
assert resolver.db_manager is not None
assert resolver.ast_service is not None
def test_get_connection(self, graphql_resolver):
"""Test database connection method."""
conn = graphql_resolver.get_connection()
assert conn is not None
cursor = conn.cursor()
cursor.execute("SELECT 1")
result = cursor.fetchone()
assert result[0] == 1
conn.close()
def test_row_to_dict(self, graphql_resolver):
"""Test row to dictionary conversion."""
conn = graphql_resolver.get_connection()
cursor = conn.cursor()
cursor.execute("SELECT 1 as test_col")
row = cursor.fetchone()
result = graphql_resolver.row_to_dict(cursor, row)
assert result == {'test_col': 1}
conn.close()
def test_resolve_markdown_file_by_id(self, graphql_query):
"""Test resolving markdown file by ID."""
result = graphql_query.resolve_markdown_file(None, id=1)
assert result is not None
assert isinstance(result, MarkdownFile)
assert result.filename == 'test.md'
assert 'Test Document' in result.content
def test_resolve_markdown_file_by_filename(self, graphql_query):
"""Test resolving markdown file by filename."""
result = graphql_query.resolve_markdown_file(None, filename='test.md')
assert result is not None
assert isinstance(result, MarkdownFile)
assert result.id == 1
def test_resolve_markdown_file_not_found(self, graphql_query):
"""Test resolving non-existent markdown file."""
result = graphql_query.resolve_markdown_file(None, id=999)
assert result is None
result = graphql_query.resolve_markdown_file(None, filename='nonexistent.md')
assert result is None
def test_resolve_schema_by_id(self, graphql_query):
"""Test resolving schema by ID."""
result = graphql_query.resolve_schema(None, id=1)
assert result is not None
assert isinstance(result, SchemaType)
assert result.title == 'Test Schema'
def test_resolve_markdown_files_list(self, graphql_query):
"""Test resolving list of markdown files."""
results = graphql_query.resolve_markdown_files(None, limit=10, offset=0)
assert isinstance(results, list)
assert len(results) >= 1
assert all(isinstance(f, MarkdownFile) for f in results)
def test_resolve_schemas_list(self, graphql_query):
"""Test resolving list of schemas."""
results = graphql_query.resolve_schemas(None, limit=10, offset=0)
assert isinstance(results, list)
assert len(results) >= 1
assert all(isinstance(s, SchemaType) for s in results)
def test_resolve_search_files(self, graphql_query):
"""Test search functionality for files."""
results = graphql_query.resolve_search(None, query="Test", type="file", limit=10)
assert isinstance(results, list)
assert len(results) >= 1
assert all(hasattr(r, 'type') and hasattr(r, 'score') for r in results)
def test_resolve_database_stats(self, graphql_query):
"""Test database statistics resolver."""
result = graphql_query.resolve_database_stats(None)
assert result is not None
assert isinstance(result, DatabaseStats)
assert result.total_files >= 1
assert result.total_schemas >= 1
assert result.total_size_bytes > 0
@patch('markitect.graphql.resolvers.Path.exists')
def test_resolve_ast_file_not_found(self, mock_exists, graphql_query):
"""Test AST resolution when file doesn't exist."""
mock_exists.return_value = False
result = graphql_query.resolve_ast(None, filename='nonexistent.md')
assert result is None
class TestGraphQLServer:
"""Test GraphQL server functionality."""
def test_server_initialization(self, temp_db_path):
"""Test server initializes correctly."""
server = GraphQLServer(db_path=temp_db_path, enable_cors=True)
assert server.db_path == temp_db_path
assert server.enable_cors is True
assert server.app is None
def test_server_initialization_without_flask(self):
"""Test server initialization when Flask is not available."""
with patch('markitect.graphql.server.FLASK_AVAILABLE', False):
with pytest.raises(ImportError, match="Flask is required"):
GraphQLServer()
def test_create_app(self, temp_db_path):
"""Test Flask app creation."""
server = GraphQLServer(db_path=temp_db_path)
app = server.create_app()
assert app is not None
assert server.app is app
def test_graphql_endpoint_post(self, flask_app):
"""Test GraphQL POST endpoint."""
with flask_app.test_client() as client:
query = '{ databaseStats { totalFiles } }'
response = client.post('/graphql',
json={'query': query},
content_type='application/json')
assert response.status_code == 200
data = response.get_json()
assert 'data' in data
assert 'databaseStats' in data['data']
def test_graphql_endpoint_invalid_json(self, flask_app):
"""Test GraphQL endpoint with invalid JSON."""
with flask_app.test_client() as client:
response = client.post('/graphql',
data='invalid json',
content_type='application/json')
# Flask returns 500 for malformed JSON, which is reasonable
assert response.status_code in [400, 500]
def test_graphql_endpoint_no_query(self, flask_app):
"""Test GraphQL endpoint without query."""
with flask_app.test_client() as client:
response = client.post('/graphql',
json={},
content_type='application/json')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
def test_graphql_playground(self, flask_app):
"""Test GraphQL playground endpoint."""
with flask_app.test_client() as client:
response = client.get('/graphql')
assert response.status_code == 200
assert 'GraphQL Playground' in response.get_data(as_text=True)
def test_schema_endpoint(self, flask_app):
"""Test schema introspection endpoint."""
with flask_app.test_client() as client:
response = client.get('/schema')
assert response.status_code == 200
data = response.get_json()
assert 'schema' in data
def test_health_check_healthy(self, flask_app):
"""Test health check endpoint when healthy."""
with flask_app.test_client() as client:
response = client.get('/health')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'healthy'
assert data['database'] == 'connected'
def test_health_check_unhealthy(self, temp_db_path):
"""Test health check when database is unavailable."""
# Use non-existent database path
server = GraphQLServer(db_path='/nonexistent/path.db')
app = server.create_app()
with app.test_client() as client:
response = client.get('/health')
assert response.status_code == 500
data = response.get_json()
assert data['status'] == 'unhealthy'
class TestGraphQLClient:
"""Test GraphQL client functionality."""
def test_client_initialization(self):
"""Test client initializes correctly."""
client = GraphQLClient("http://localhost:5000/graphql")
assert client.endpoint == "http://localhost:5000/graphql"
def test_client_default_endpoint(self):
"""Test client uses default endpoint."""
client = GraphQLClient()
assert client.endpoint == "http://localhost:5000/graphql"
@patch('requests.post')
def test_client_execute_query(self, mock_post):
"""Test client query execution."""
# Mock response
mock_response = Mock()
mock_response.json.return_value = {
'data': {'databaseStats': {'totalFiles': 5}}
}
mock_post.return_value = mock_response
client = GraphQLClient()
result = client.execute('{ databaseStats { totalFiles } }')
assert result['data']['databaseStats']['totalFiles'] == 5
mock_post.assert_called_once()
def test_client_execute_local(self, temp_db_path):
"""Test client local query execution."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
client = GraphQLClient()
result = client.execute_local('{ databaseStats { totalFiles } }', context={'db_path': temp_db_path})
assert result is not None
assert 'data' in result
# The databaseStats resolver might return None if db is empty, so let's be more flexible
if result['data']['databaseStats'] is not None:
assert result['data']['databaseStats']['totalFiles'] >= 0
def test_client_execute_without_requests(self):
"""Test client execution when requests is not available."""
import builtins
original_import = builtins.__import__
def mock_import(name, *args, **kwargs):
if name == 'requests':
raise ImportError("No module named 'requests'")
return original_import(name, *args, **kwargs)
with patch('builtins.__import__', side_effect=mock_import):
client = GraphQLClient()
with pytest.raises(ImportError, match="requests is required"):
client.execute('{ databaseStats { totalFiles } }')
class TestGraphQLQueries:
"""Test actual GraphQL query execution."""
def test_simple_database_stats_query(self, temp_db_path):
"""Test simple database stats query."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = """
{
databaseStats {
totalFiles
totalSchemas
totalSizeBytes
}
}
"""
result = schema.execute(query, context={'db_path': temp_db_path})
assert result.errors is None
assert result.data is not None
assert 'databaseStats' in result.data
if result.data['databaseStats'] is not None:
assert result.data['databaseStats']['totalFiles'] >= 1
assert result.data['databaseStats']['totalSchemas'] >= 1
def test_markdown_file_query_with_computed_fields(self, temp_db_path):
"""Test markdown file query with computed fields."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = """
{
markdownFile(id: 1) {
id
filename
content
wordCount
lineCount
hasFrontMatter
frontMatter {
key
value
}
}
}
"""
result = schema.execute(query, context={'db_path': temp_db_path})
assert result.errors is None
assert result.data is not None
data = result.data['markdownFile']
if data is not None:
assert data['id'] == 1
assert data['filename'] == 'test.md'
assert data['wordCount'] > 0
assert data['lineCount'] > 0
assert data['hasFrontMatter'] is True
assert len(data['frontMatter']) > 0
def test_search_query(self, temp_db_path):
"""Test search functionality."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = """
{
search(query: "Test", type: "all", limit: 10) {
type
score
file {
filename
}
schema {
title
}
highlight
}
}
"""
result = schema.execute(query, context={'db_path': temp_db_path})
assert result.errors is None
assert result.data is not None
if result.data['search'] is not None:
assert len(result.data['search']) >= 0
def test_pagination_query(self, temp_db_path):
"""Test pagination in list queries."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = """
{
markdownFiles(limit: 1, offset: 0) {
id
filename
}
}
"""
result = schema.execute(query, context={'db_path': temp_db_path})
assert result.errors is None
assert result.data is not None
if result.data['markdownFiles'] is not None:
assert len(result.data['markdownFiles']) <= 1
@pytest.mark.e2e
class TestGraphQLCLIIntegration:
"""Test GraphQL CLI command integration."""
def test_graphql_schema_command(self, isolated_environment):
"""Test graphql-schema CLI command."""
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-schema", "--local"],
env=isolated_environment,
capture_output=True,
text=True,
cwd=Path.cwd()
)
assert result.returncode == 0
assert "type Query" in result.stdout
def test_graphql_query_command(self, isolated_environment):
"""Test graphql-query CLI command."""
query = "{ databaseStats { totalFiles } }"
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-query", query, "--local"],
env=isolated_environment,
capture_output=True,
text=True,
cwd=Path.cwd()
)
assert result.returncode == 0
# The database might be empty in test environment, so check for JSON structure
assert "databaseStats" in result.stdout
def test_graphql_examples_command(self, isolated_environment):
"""Test graphql-examples CLI command."""
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-examples"],
env=isolated_environment,
capture_output=True,
text=True,
cwd=Path.cwd()
)
assert result.returncode == 0
assert "GraphQL Query Examples" in result.stdout
assert "databaseStats" in result.stdout
@patch('markitect.graphql.server.GraphQLServer')
def test_graphql_serve_command(self, mock_server_class, isolated_environment):
"""Test graphql-serve CLI command."""
mock_server = Mock()
mock_server_class.return_value = mock_server
# We can't actually start the server in tests, so we just test command parsing
result = subprocess.run(
[sys.executable, "-m", "markitect.cli", "graphql-serve", "--help"],
env=isolated_environment,
capture_output=True,
text=True,
cwd=Path.cwd()
)
assert result.returncode == 0
assert "Start GraphQL server" in result.stdout
class TestErrorHandling:
"""Test error handling in GraphQL interface."""
def test_invalid_query_syntax(self, temp_db_path):
"""Test handling of invalid GraphQL syntax."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = "{ invalidSyntax }"
result = schema.execute(query)
assert result.errors is not None
assert len(result.errors) > 0
def test_nonexistent_field_query(self, temp_db_path):
"""Test querying nonexistent fields."""
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = "{ nonexistentField }"
result = schema.execute(query)
assert result.errors is not None
def test_resolver_database_error(self, temp_db_path):
"""Test resolver behavior when database is corrupted."""
# Corrupt the database file
with open(temp_db_path, 'w') as f:
f.write("corrupted data")
with patch('markitect.graphql.resolvers.get_default_database_path', return_value=temp_db_path):
query = "{ databaseStats { totalFiles } }"
result = schema.execute(query, context={'db_path': temp_db_path})
# Should handle database errors gracefully - either with errors or None data
assert result.errors is not None or result.data['databaseStats'] is None
class TestUtilityFunctions:
"""Test utility functions in GraphQL module."""
def test_get_default_database_path_with_env(self):
"""Test get_default_database_path with environment variable."""
with patch.dict(os.environ, {'MARKITECT_DB': '/custom/path.db'}):
path = get_default_database_path()
assert path == '/custom/path.db'
def test_get_default_database_path_default(self):
"""Test get_default_database_path with default location."""
with patch.dict(os.environ, {}, clear=True):
path = get_default_database_path()
assert path.endswith('markitect.db')
assert '.markitect' in path