Aniworld/tests/unit/web/controllers/shared/test_validators.py

546 lines
19 KiB
Python

"""
Test cases for input validation utilities.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from flask import Flask, request
import json
import tempfile
import os
# Import the modules to test
try:
from src.server.web.controllers.shared.validators import (
validate_json_input, validate_query_params, validate_pagination_params,
validate_anime_data, validate_file_upload, is_valid_url, is_valid_email,
sanitize_string, validate_id_parameter
)
except ImportError:
# Fallback for testing
validate_json_input = None
validate_query_params = None
validate_pagination_params = None
validate_anime_data = None
validate_file_upload = None
is_valid_url = None
is_valid_email = None
sanitize_string = None
validate_id_parameter = None
class TestValidationDecorators:
"""Test cases for validation decorators."""
@pytest.fixture
def app(self):
"""Create a test Flask application."""
app = Flask(__name__)
app.config['TESTING'] = True
return app
@pytest.fixture
def client(self, app):
"""Create a test client."""
return app.test_client()
def test_validate_json_input_success(self, app, client):
"""Test validate_json_input decorator with valid JSON."""
if not validate_json_input:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(
required_fields=['name'],
optional_fields=['description'],
field_types={'name': str, 'description': str}
)
def test_endpoint():
return {'status': 'success'}
response = client.post('/test',
json={'name': 'Test Name', 'description': 'Test Description'},
content_type='application/json')
assert response.status_code == 200
def test_validate_json_input_missing_required(self, app, client):
"""Test validate_json_input with missing required field."""
if not validate_json_input:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(required_fields=['name'])
def test_endpoint():
return {'status': 'success'}
response = client.post('/test',
json={'description': 'Test Description'},
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Missing required fields' in data[0]['message']
def test_validate_json_input_wrong_type(self, app, client):
"""Test validate_json_input with wrong field type."""
if not validate_json_input:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(
required_fields=['age'],
field_types={'age': int}
)
def test_endpoint():
return {'status': 'success'}
response = client.post('/test',
json={'age': 'twenty'},
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Type validation failed' in data[0]['message']
def test_validate_json_input_unexpected_fields(self, app, client):
"""Test validate_json_input with unexpected fields."""
if not validate_json_input:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(
required_fields=['name'],
optional_fields=['description']
)
def test_endpoint():
return {'status': 'success'}
response = client.post('/test',
json={'name': 'Test', 'description': 'Test', 'extra_field': 'unexpected'},
content_type='application/json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Unexpected fields' in data[0]['message']
def test_validate_json_input_not_json(self, app, client):
"""Test validate_json_input with non-JSON content."""
if not validate_json_input:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(required_fields=['name'])
def test_endpoint():
return {'status': 'success'}
response = client.post('/test', data='not json')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Request must be JSON' in data[0]['message']
def test_validate_query_params_success(self, app, client):
"""Test validate_query_params decorator with valid parameters."""
if not validate_query_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_query_params(
allowed_params=['page', 'limit'],
required_params=['page'],
param_types={'page': int, 'limit': int}
)
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?page=1&limit=10')
assert response.status_code == 200
def test_validate_query_params_missing_required(self, app, client):
"""Test validate_query_params with missing required parameter."""
if not validate_query_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_query_params(required_params=['page'])
def test_endpoint():
return {'status': 'success'}
response = client.get('/test')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Missing required parameters' in data[0]['message']
def test_validate_query_params_unexpected(self, app, client):
"""Test validate_query_params with unexpected parameters."""
if not validate_query_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_query_params(allowed_params=['page'])
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?page=1&unexpected=value')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Unexpected parameters' in data[0]['message']
def test_validate_query_params_wrong_type(self, app, client):
"""Test validate_query_params with wrong parameter type."""
if not validate_query_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_query_params(
allowed_params=['page'],
param_types={'page': int}
)
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?page=abc')
assert response.status_code == 400
data = json.loads(response.data)
assert 'Parameter type validation failed' in data[0]['message']
def test_validate_pagination_params_success(self, app, client):
"""Test validate_pagination_params decorator with valid parameters."""
if not validate_pagination_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_pagination_params
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?page=1&per_page=10&limit=20&offset=5')
assert response.status_code == 200
def test_validate_pagination_params_invalid_page(self, app, client):
"""Test validate_pagination_params with invalid page."""
if not validate_pagination_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_pagination_params
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?page=0')
assert response.status_code == 400
data = json.loads(response.data)
assert 'page must be greater than 0' in data[0]['errors']
def test_validate_pagination_params_invalid_per_page(self, app, client):
"""Test validate_pagination_params with invalid per_page."""
if not validate_pagination_params:
pytest.skip("Module not available")
@app.route('/test')
@validate_pagination_params
def test_endpoint():
return {'status': 'success'}
response = client.get('/test?per_page=2000')
assert response.status_code == 400
data = json.loads(response.data)
assert 'per_page cannot exceed 1000' in data[0]['errors']
def test_validate_id_parameter_success(self, app, client):
"""Test validate_id_parameter decorator with valid ID."""
if not validate_id_parameter:
pytest.skip("Module not available")
@app.route('/test/<int:id>')
@validate_id_parameter('id')
def test_endpoint(id):
return {'status': 'success', 'id': id}
response = client.get('/test/123')
assert response.status_code == 200
data = json.loads(response.data)
assert data['id'] == 123
def test_validate_id_parameter_invalid(self, app):
"""Test validate_id_parameter decorator with invalid ID."""
if not validate_id_parameter:
pytest.skip("Module not available")
@validate_id_parameter('id')
def test_function(id='abc'):
return {'status': 'success'}
# Since this is a decorator that modifies kwargs,
# we test it directly
result = test_function(id='abc')
# Should return error response
assert result[1] == 400
class TestValidationUtilities:
"""Test cases for validation utility functions."""
def test_validate_anime_data_valid(self):
"""Test validate_anime_data with valid data."""
if not validate_anime_data:
pytest.skip("Module not available")
data = {
'name': 'Test Anime',
'url': 'https://example.com/anime/test',
'description': 'A test anime',
'episodes': 12,
'status': 'completed'
}
errors = validate_anime_data(data)
assert len(errors) == 0
def test_validate_anime_data_missing_required(self):
"""Test validate_anime_data with missing required fields."""
if not validate_anime_data:
pytest.skip("Module not available")
data = {
'description': 'A test anime'
}
errors = validate_anime_data(data)
assert len(errors) > 0
assert any('Missing required field: name' in error for error in errors)
assert any('Missing required field: url' in error for error in errors)
def test_validate_anime_data_invalid_types(self):
"""Test validate_anime_data with invalid field types."""
if not validate_anime_data:
pytest.skip("Module not available")
data = {
'name': 123, # Should be string
'url': 'invalid-url', # Should be valid URL
'episodes': 'twelve' # Should be integer
}
errors = validate_anime_data(data)
assert len(errors) > 0
assert any('name must be a string' in error for error in errors)
assert any('url must be a valid URL' in error for error in errors)
assert any('episodes must be an integer' in error for error in errors)
def test_validate_anime_data_invalid_status(self):
"""Test validate_anime_data with invalid status."""
if not validate_anime_data:
pytest.skip("Module not available")
data = {
'name': 'Test Anime',
'url': 'https://example.com/anime/test',
'status': 'invalid_status'
}
errors = validate_anime_data(data)
assert len(errors) > 0
assert any('status must be one of' in error for error in errors)
def test_validate_file_upload_valid(self):
"""Test validate_file_upload with valid file."""
if not validate_file_upload:
pytest.skip("Module not available")
# Create a mock file object
mock_file = Mock()
mock_file.filename = 'test.txt'
mock_file.content_length = 1024 # 1KB
errors = validate_file_upload(mock_file, allowed_extensions=['txt'], max_size_mb=1)
assert len(errors) == 0
def test_validate_file_upload_no_file(self):
"""Test validate_file_upload with no file."""
if not validate_file_upload:
pytest.skip("Module not available")
errors = validate_file_upload(None)
assert len(errors) > 0
assert 'No file provided' in errors
def test_validate_file_upload_empty_filename(self):
"""Test validate_file_upload with empty filename."""
if not validate_file_upload:
pytest.skip("Module not available")
mock_file = Mock()
mock_file.filename = ''
errors = validate_file_upload(mock_file)
assert len(errors) > 0
assert 'No file selected' in errors
def test_validate_file_upload_invalid_extension(self):
"""Test validate_file_upload with invalid extension."""
if not validate_file_upload:
pytest.skip("Module not available")
mock_file = Mock()
mock_file.filename = 'test.exe'
errors = validate_file_upload(mock_file, allowed_extensions=['txt', 'pdf'])
assert len(errors) > 0
assert 'File type not allowed' in errors[0]
def test_validate_file_upload_too_large(self):
"""Test validate_file_upload with file too large."""
if not validate_file_upload:
pytest.skip("Module not available")
mock_file = Mock()
mock_file.filename = 'test.txt'
mock_file.content_length = 5 * 1024 * 1024 # 5MB
errors = validate_file_upload(mock_file, max_size_mb=1)
assert len(errors) > 0
assert 'File size exceeds maximum' in errors[0]
def test_is_valid_url_valid(self):
"""Test is_valid_url with valid URLs."""
if not is_valid_url:
pytest.skip("Module not available")
valid_urls = [
'https://example.com',
'http://test.co.uk',
'https://subdomain.example.com/path',
'http://localhost:8080',
'https://192.168.1.1:3000/api'
]
for url in valid_urls:
assert is_valid_url(url), f"URL should be valid: {url}"
def test_is_valid_url_invalid(self):
"""Test is_valid_url with invalid URLs."""
if not is_valid_url:
pytest.skip("Module not available")
invalid_urls = [
'not-a-url',
'ftp://example.com', # Only http/https supported
'https://',
'http://.',
'just-text'
]
for url in invalid_urls:
assert not is_valid_url(url), f"URL should be invalid: {url}"
def test_is_valid_email_valid(self):
"""Test is_valid_email with valid emails."""
if not is_valid_email:
pytest.skip("Module not available")
valid_emails = [
'test@example.com',
'user.name@domain.co.uk',
'admin+tag@site.org',
'user123@test-domain.com'
]
for email in valid_emails:
assert is_valid_email(email), f"Email should be valid: {email}"
def test_is_valid_email_invalid(self):
"""Test is_valid_email with invalid emails."""
if not is_valid_email:
pytest.skip("Module not available")
invalid_emails = [
'not-an-email',
'@domain.com',
'user@',
'user@domain',
'user space@domain.com'
]
for email in invalid_emails:
assert not is_valid_email(email), f"Email should be invalid: {email}"
def test_sanitize_string_basic(self):
"""Test sanitize_string with basic input."""
if not sanitize_string:
pytest.skip("Module not available")
result = sanitize_string(" Hello World ")
assert result == "Hello World"
def test_sanitize_string_max_length(self):
"""Test sanitize_string with max length."""
if not sanitize_string:
pytest.skip("Module not available")
long_string = "A" * 100
result = sanitize_string(long_string, max_length=50)
assert len(result) == 50
assert result == "A" * 50
def test_sanitize_string_control_characters(self):
"""Test sanitize_string removes control characters."""
if not sanitize_string:
pytest.skip("Module not available")
input_string = "Hello\x00World\x01Test"
result = sanitize_string(input_string)
assert result == "HelloWorldTest"
def test_sanitize_string_non_string(self):
"""Test sanitize_string with non-string input."""
if not sanitize_string:
pytest.skip("Module not available")
result = sanitize_string(123)
assert result == "123"
class TestValidatorIntegration:
"""Integration tests for validators."""
@pytest.fixture
def app(self):
"""Create a test Flask application."""
app = Flask(__name__)
app.config['TESTING'] = True
return app
@pytest.fixture
def client(self, app):
"""Create a test client."""
return app.test_client()
def test_multiple_validators(self, app, client):
"""Test using multiple validators on the same endpoint."""
if not validate_json_input or not validate_pagination_params:
pytest.skip("Module not available")
@app.route('/test', methods=['POST'])
@validate_json_input(required_fields=['name'])
@validate_pagination_params
def test_endpoint():
return {'status': 'success'}
response = client.post('/test?page=1&per_page=10',
json={'name': 'Test'},
content_type='application/json')
assert response.status_code == 200
def test_validator_preserves_metadata(self):
"""Test that validators preserve function metadata."""
if not validate_json_input:
pytest.skip("Module not available")
@validate_json_input(required_fields=['name'])
def test_function():
"""Test function docstring."""
return "test"
assert test_function.__name__ == "test_function"
assert test_function.__doc__ == "Test function docstring."
if __name__ == '__main__':
pytest.main([__file__])