""" Security-focused tests for DMARQ application. Covers API key management, domain validation, file upload limits, and XML parsing security. """ import pytest from fastapi.testclient import TestClient import app.services.dmarc_parser as parser_module from app.core.security import add_api_key, generate_api_key, verify_api_key from app.main import create_app from app.services.dmarc_parser import DMARCParser from app.utils.domain_validator import validate_domain, validate_domain_config class TestAPIKeySecurity: """Test API key generation and verification.""" def test_generate_api_key_length_and_uniqueness(self): """Generated keys should be 64 hex characters and unique.""" key1 = generate_api_key() key2 = generate_api_key() assert len(key1) == 64 assert len(key2) == 64 assert key1 != key2 assert all(c in "0123456789abcdef" for c in key1) def test_add_and_verify_api_key(self): """Keys should only be valid after being added.""" key = generate_api_key() assert not verify_api_key(key) assert add_api_key(key) is True assert verify_api_key(key) is True # Adding the same key again returns False assert add_api_key(key) is False class TestDomainValidation: """Test domain name validation.""" @pytest.mark.parametrize( "domain", [ "example.com", "subdomain.example.com", "my-domain.example.org", "test123.example.net", ], ) def test_valid_domains(self, domain): is_valid, error, _ = validate_domain(domain, check_dns=False) assert is_valid, f"Domain {domain} should be valid: {error}" @pytest.mark.parametrize( "domain", [ "", " ", "example", "-example.com", "example-.com", "exam ple.com", "example..com", "a" * 64 + ".com", "a" * 254, ], ) def test_invalid_domain_format(self, domain): is_valid, error, _ = validate_domain(domain, check_dns=False) assert not is_valid, f"Domain '{domain}' should be invalid" assert error is not None @pytest.mark.parametrize( "domain", [ "example.com"} ) assert not result["valid"] assert "description" in result["errors"] class TestFileUploadSecurity: """Test file upload size limits.""" def test_file_size_limit(self): large_content = b"x" * (11 * 1024 * 1024) with pytest.raises(ValueError, match="too large"): DMARCParser.parse_file(large_content, "test.xml") class TestXMLParsingSecurity: """Test XML parsing security (defusedxml, XXE protection).""" def test_defusedxml_is_used(self): assert hasattr(parser_module, "ET") module_info = str(getattr(parser_module.ET, "__name__", "")) + str( getattr(parser_module.ET, "__module__", "") ) assert "defusedxml" in module_info.lower() def test_xxe_protection(self): """defusedxml should prevent XXE entity expansion.""" xxe_payload = b"""\ ]> &xxe; """ # defusedxml should raise an error or not expand the entity try: result = DMARCParser.parse_file(xxe_payload, "test.xml") org_name = result.get("org_name", "") assert "root:" not in org_name and "/bin" not in org_name except Exception: # pylint: disable=broad-exception-caught pass # Expected – defusedxml blocks DTD processing class TestAdminApiKeyStartup: """Test admin API key loading during the application startup event.""" def test_startup_uses_env_api_key(self, monkeypatch): """When ADMIN_API_KEY is configured, startup should register it directly.""" import app.core.security as sec_module import app.main as main_module test_key = "a" * 64 monkeypatch.setattr(main_module.settings, "ADMIN_API_KEY", test_key) saved_keys = set(sec_module._api_keys) sec_module._api_keys.clear() try: application = create_app() with TestClient(application): assert sec_module.verify_api_key(test_key) finally: sec_module._api_keys.clear() sec_module._api_keys.update(saved_keys) def test_startup_generates_key_when_no_env(self, monkeypatch): """When ADMIN_API_KEY is not set, startup should generate a random key.""" import app.core.security as sec_module import app.main as main_module monkeypatch.setattr(main_module.settings, "ADMIN_API_KEY", None) saved_keys = set(sec_module._api_keys) sec_module._api_keys.clear() try: application = create_app() with TestClient(application): assert len(sec_module._api_keys) == 1 finally: sec_module._api_keys.clear() sec_module._api_keys.update(saved_keys)