Medium-MCP / tests /unit /test_validation.py
Nikhil Pravin Pise
feat: implement comprehensive improvement plan (Phases 1-5)
e98cc10
"""
Unit Tests for Validation Module
Tests for URL validation, query validation, and input sanitization.
"""
from __future__ import annotations
import pytest
from src.validation import (
validate_url,
validate_medium_url,
validate_batch_urls,
validate_search_query,
validate_tag,
validate_positive_int,
validate_post_id,
ValidationResult,
)
class TestURLValidation:
"""Tests for URL validation."""
def test_valid_http_url(self) -> None:
"""Valid HTTP URL should pass."""
result = validate_url("http://example.com/article")
assert result.is_valid
assert result.value == "http://example.com/article"
def test_valid_https_url(self) -> None:
"""Valid HTTPS URL should pass."""
result = validate_url("https://medium.com/@user/article")
assert result.is_valid
def test_empty_url_fails(self) -> None:
"""Empty URL should fail."""
result = validate_url("")
assert not result.is_valid
assert "required" in result.error.lower()
def test_javascript_url_blocked(self) -> None:
"""JavaScript URLs should be blocked."""
result = validate_url("javascript:alert('xss')")
assert not result.is_valid
assert "dangerous" in result.error.lower()
def test_data_url_blocked(self) -> None:
"""Data URLs should be blocked."""
result = validate_url("data:text/html,<script>alert('xss')</script>")
assert not result.is_valid
def test_too_long_url_fails(self) -> None:
"""URLs over max length should fail."""
long_url = "https://example.com/" + "a" * 3000
result = validate_url(long_url)
assert not result.is_valid
assert "length" in result.error.lower()
def test_invalid_scheme_fails(self) -> None:
"""Invalid schemes should fail."""
result = validate_url("ftp://example.com/file")
assert not result.is_valid
assert "http" in result.error.lower()
def test_url_with_whitespace_trimmed(self) -> None:
"""Whitespace should be trimmed."""
result = validate_url(" https://medium.com/article ")
assert result.is_valid
assert result.value == "https://medium.com/article"
class TestMediumURLValidation:
"""Tests for Medium-specific URL validation."""
def test_valid_medium_url(self) -> None:
"""Medium.com URLs should pass."""
result = validate_medium_url("https://medium.com/@user/article")
assert result.is_valid
def test_valid_publication_url(self) -> None:
"""Publication URLs should pass."""
result = validate_medium_url("https://towardsdatascience.com/article")
assert result.is_valid
def test_non_medium_url_fails(self) -> None:
"""Non-Medium URLs should fail."""
result = validate_medium_url("https://google.com/search")
assert not result.is_valid
assert "Medium" in result.error
class TestBatchURLValidation:
"""Tests for batch URL validation."""
def test_valid_batch(self) -> None:
"""Valid batch should return valid URLs."""
urls = [
"https://medium.com/article1",
"https://medium.com/article2",
]
valid, errors = validate_batch_urls(urls)
assert len(valid) == 2
assert len(errors) == 0
def test_mixed_batch(self) -> None:
"""Mixed batch should separate valid and invalid."""
urls = [
"https://medium.com/article1",
"invalid-url",
"https://medium.com/article2",
]
valid, errors = validate_batch_urls(urls)
assert len(valid) == 2
assert len(errors) == 1
def test_empty_batch(self) -> None:
"""Empty batch should return error."""
valid, errors = validate_batch_urls([])
assert len(valid) == 0
assert len(errors) == 1
def test_batch_size_limit(self) -> None:
"""Batch over limit should fail."""
urls = [f"https://medium.com/article{i}" for i in range(25)]
valid, errors = validate_batch_urls(urls)
assert len(valid) == 0
assert "maximum" in errors[0]["error"].lower()
class TestSearchQueryValidation:
"""Tests for search query validation."""
def test_valid_query(self) -> None:
"""Valid query should pass."""
result = validate_search_query("python async programming")
assert result.is_valid
assert result.value == "python async programming"
def test_empty_query_fails(self) -> None:
"""Empty query should fail."""
result = validate_search_query("")
assert not result.is_valid
def test_short_query_fails(self) -> None:
"""Query under 2 chars should fail."""
result = validate_search_query("a")
assert not result.is_valid
def test_query_sanitization(self) -> None:
"""Dangerous characters should be removed."""
result = validate_search_query("<script>evil</script>")
assert result.is_valid
assert "<" not in result.value
assert ">" not in result.value
assert result.sanitized
class TestTagValidation:
"""Tests for tag validation."""
def test_valid_tag(self) -> None:
"""Valid tag should pass."""
result = validate_tag("python")
assert result.is_valid
assert result.value == "python"
def test_tag_with_hyphen(self) -> None:
"""Tags with hyphens should pass."""
result = validate_tag("machine-learning")
assert result.is_valid
def test_tag_sanitization(self) -> None:
"""Tags should be sanitized."""
result = validate_tag("Machine Learning")
assert result.is_valid
assert result.value == "machine-learning"
def test_empty_tag_fails(self) -> None:
"""Empty tag should fail."""
result = validate_tag("")
assert not result.is_valid
class TestNumericValidation:
"""Tests for numeric validation."""
def test_valid_int(self) -> None:
"""Valid integer should pass."""
result = validate_positive_int(5, "count", 1, 10)
assert result.is_valid
assert result.value == "5"
def test_below_min_fails(self) -> None:
"""Value below min should fail."""
result = validate_positive_int(0, "count", 1, 10)
assert not result.is_valid
def test_above_max_fails(self) -> None:
"""Value above max should fail."""
result = validate_positive_int(100, "count", 1, 10)
assert not result.is_valid
def test_non_int_fails(self) -> None:
"""Non-integer should fail."""
result = validate_positive_int("abc", "count")
assert not result.is_valid
class TestPostIDValidation:
"""Tests for post ID validation."""
def test_valid_post_id(self) -> None:
"""Valid hex post ID should pass."""
result = validate_post_id("abc123def456")
assert result.is_valid
def test_short_post_id_fails(self) -> None:
"""Short post ID should fail."""
result = validate_post_id("abc")
assert not result.is_valid
def test_non_hex_fails(self) -> None:
"""Non-hex characters should fail."""
result = validate_post_id("xyz123ghijkl")
assert not result.is_valid
def test_empty_post_id_fails(self) -> None:
"""Empty post ID should fail."""
result = validate_post_id("")
assert not result.is_valid