546 lines
19 KiB
Python
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__]) |