Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
195 lines
7.5 KiB
Python
195 lines
7.5 KiB
Python
"""
|
|
Tests for webhook URL validation.
|
|
Story 3.7: Webhook - Spécification URL
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
|
|
class TestWebhookURLValidator:
|
|
"""Tests for WebhookURLValidator class."""
|
|
|
|
@pytest.fixture
|
|
def validator(self):
|
|
"""Create a WebhookURLValidator instance."""
|
|
from middleware.validation import WebhookURLValidator
|
|
return WebhookURLValidator()
|
|
|
|
def test_valid_https_url(self, validator):
|
|
"""Valid HTTPS URL should pass."""
|
|
url = "https://example.com/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is True
|
|
assert error is None
|
|
assert details is None
|
|
|
|
def test_valid_http_url(self, validator):
|
|
"""Valid HTTP URL should pass."""
|
|
url = "http://example.com/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is True
|
|
assert error is None
|
|
|
|
def test_invalid_scheme_ftp(self, validator):
|
|
"""FTP URL should be rejected."""
|
|
url = "ftp://example.com/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False
|
|
assert "http" in error.lower()
|
|
|
|
def test_invalid_scheme_no_scheme(self, validator):
|
|
"""URL without scheme should be rejected."""
|
|
url = "example.com/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False
|
|
|
|
def test_localhost_blocked(self, validator):
|
|
"""Localhost should be blocked."""
|
|
urls = [
|
|
"http://localhost/webhook",
|
|
"http://127.0.0.1/webhook",
|
|
"http://0.0.0.0/webhook",
|
|
]
|
|
for url in urls:
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False, f"URL {url} should be blocked"
|
|
assert "localhost" in error.lower() or "priv" in error.lower() or "non autoris" in error.lower()
|
|
|
|
def test_credentials_in_url_blocked(self, validator):
|
|
"""URLs with credentials should be blocked."""
|
|
url = "https://user:password@example.com/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False
|
|
assert "credentials" in error.lower() or "identifiants" in error.lower()
|
|
|
|
def test_empty_url_valid(self, validator):
|
|
"""Empty URL should be valid (optional parameter)."""
|
|
is_valid, error, details = validator.validate("")
|
|
assert is_valid is True
|
|
|
|
def test_none_url_valid(self, validator):
|
|
"""None URL should be valid (optional parameter)."""
|
|
is_valid, error, details = validator.validate(None)
|
|
assert is_valid is True
|
|
|
|
def test_url_with_port(self, validator):
|
|
"""URL with port should be valid."""
|
|
url = "https://example.com:8080/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is True
|
|
|
|
def test_url_with_query_params(self, validator):
|
|
"""URL with query parameters should be valid."""
|
|
url = "https://example.com/webhook?token=abc123&source=api"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is True
|
|
|
|
def test_url_with_path(self, validator):
|
|
"""URL with path should be valid."""
|
|
url = "https://example.com/api/v1/notifications/translation-complete"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is True
|
|
|
|
def test_url_missing_hostname(self, validator):
|
|
"""URL without hostname should be rejected."""
|
|
url = "https:///webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False
|
|
assert "hostname" in error.lower() or "hôte" in error.lower()
|
|
|
|
def test_ipv6_localhost_blocked(self, validator):
|
|
"""IPv6 localhost should be blocked."""
|
|
url = "http://[::1]/webhook"
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False
|
|
|
|
def test_private_ip_blocked(self, validator):
|
|
"""Private IP addresses should be blocked."""
|
|
private_ips = [
|
|
"http://10.0.0.1/webhook",
|
|
"http://172.16.0.1/webhook",
|
|
"http://192.168.1.1/webhook",
|
|
]
|
|
for url in private_ips:
|
|
is_valid, error, details = validator.validate(url)
|
|
assert is_valid is False, f"URL {url} should be blocked"
|
|
assert "priv" in error.lower() or "non autoris" in error.lower()
|
|
|
|
|
|
class TestWebhookURLIntegration:
|
|
"""Integration tests for webhook URL in translate endpoint."""
|
|
|
|
@pytest.fixture
|
|
def client(self):
|
|
"""Create test client."""
|
|
from fastapi.testclient import TestClient
|
|
from main import app
|
|
return TestClient(app)
|
|
|
|
@pytest.fixture
|
|
def auth_headers(self):
|
|
"""Create auth headers for testing."""
|
|
# This would need a valid token in real tests
|
|
return {"Authorization": "Bearer test_token"}
|
|
|
|
@pytest.fixture
|
|
def sample_file(self, tmp_path):
|
|
"""Create a sample Excel file for testing."""
|
|
import zipfile
|
|
file_path = tmp_path / "test.xlsx"
|
|
# Create a minimal valid xlsx file (ZIP with correct magic bytes)
|
|
with zipfile.ZipFile(file_path, 'w') as zf:
|
|
zf.writestr("[Content_Types].xml", '<?xml version="1.0"?>')
|
|
return file_path
|
|
|
|
def test_translate_with_valid_webhook_url(self, client, sample_file):
|
|
"""Translation with valid webhook_url should succeed."""
|
|
# This test would need proper authentication setup
|
|
# For now, we test the validation logic directly
|
|
from middleware.validation import webhook_validator
|
|
|
|
url = "https://example.com/webhook"
|
|
is_valid, error, details = webhook_validator.validate(url)
|
|
assert is_valid is True
|
|
|
|
def test_translate_with_invalid_webhook_url(self, client):
|
|
"""Translation with invalid webhook_url should return 400."""
|
|
from middleware.validation import webhook_validator
|
|
|
|
url = "ftp://example.com/webhook"
|
|
is_valid, error, details = webhook_validator.validate(url)
|
|
assert is_valid is False
|
|
assert "http" in error.lower()
|
|
|
|
def test_translate_without_webhook_url(self, client):
|
|
"""Translation without webhook_url should succeed."""
|
|
from middleware.validation import webhook_validator
|
|
|
|
# Empty webhook URL should be valid (optional)
|
|
is_valid, error, details = webhook_validator.validate("")
|
|
assert is_valid is True
|
|
|
|
is_valid, error, details = webhook_validator.validate(None)
|
|
assert is_valid is True
|
|
|
|
|
|
class TestWebhookValidatorSingleton:
|
|
"""Tests for the webhook_validator singleton instance."""
|
|
|
|
def test_singleton_exists(self):
|
|
"""The webhook_validator singleton should be available."""
|
|
from middleware.validation import webhook_validator
|
|
assert webhook_validator is not None
|
|
|
|
def test_singleton_is_validator(self):
|
|
"""The webhook_validator should be a WebhookURLValidator instance."""
|
|
from middleware.validation import webhook_validator, WebhookURLValidator
|
|
assert isinstance(webhook_validator, WebhookURLValidator)
|
|
|
|
def test_singleton_default_settings(self):
|
|
"""The singleton should have default security settings enabled."""
|
|
from middleware.validation import webhook_validator
|
|
assert webhook_validator.block_private_ips is True
|
|
assert "http" in webhook_validator.allowed_schemes
|
|
assert "https" in webhook_validator.allowed_schemes |