37b3bf8db2
- test_url_security: URL安全校验(SSRF防护/魔数校验/可信域名/直接IP拦截) ~85个 - test_pagination: 分页器(参数校验/偏移计算/元数据/内存分页) ~35个 - test_text_splitter: 文本分段器(句子边界/强制切段/短段合并) ~24个 全部纯逻辑,无外部依赖
533 lines
21 KiB
Python
Executable File
533 lines
21 KiB
Python
Executable File
"""URL安全校验纯逻辑测试 — SSRF防护/魔数校验/可信域名.
|
||
|
||
聚焦纯函数校验逻辑,DNS解析和实际下载用mock隔离。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import ipaddress
|
||
import os
|
||
import tempfile
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
|
||
from shared.url_security import (
|
||
ALLOWED_PORTS,
|
||
ALLOWED_SCHEMES,
|
||
UrlSecurityError,
|
||
_check_internal_hostnames,
|
||
_check_ssrf_ip,
|
||
_is_trusted_domain,
|
||
_validate_magic_number,
|
||
is_url_safe,
|
||
validate_url_safety,
|
||
)
|
||
|
||
|
||
class TestUrlSecurityError:
|
||
"""UrlSecurityError 异常类."""
|
||
|
||
def test_is_value_error(self):
|
||
"""继承自 ValueError."""
|
||
assert issubclass(UrlSecurityError, ValueError)
|
||
|
||
def test_message_preserved(self):
|
||
"""错误消息被保留."""
|
||
err = UrlSecurityError("test message")
|
||
assert str(err) == "test message"
|
||
|
||
|
||
class TestValidateUrlSafetyBasic:
|
||
"""validate_url_safety 基础校验(不涉及DNS解析)."""
|
||
|
||
def test_empty_url_rejected(self):
|
||
"""空URL被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="URL 为空"):
|
||
validate_url_safety("")
|
||
|
||
def test_none_url_rejected(self):
|
||
"""None URL被拒绝."""
|
||
with pytest.raises(UrlSecurityError):
|
||
validate_url_safety(None)
|
||
|
||
def test_url_too_long_rejected(self):
|
||
"""超长URL被拒绝."""
|
||
long_url = "https://example.com/" + "a" * 2048
|
||
with pytest.raises(UrlSecurityError, match="URL 过长"):
|
||
validate_url_safety(long_url)
|
||
|
||
def test_invalid_scheme_rejected(self):
|
||
"""非法scheme被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="不允许的 URL scheme"):
|
||
validate_url_safety("ftp://example.com/file.mp3")
|
||
|
||
def test_file_scheme_rejected(self):
|
||
"""file:// scheme被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="不允许的 URL scheme"):
|
||
validate_url_safety("file:///etc/passwd")
|
||
|
||
def test_no_scheme_rejected(self):
|
||
"""无scheme被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="不允许的 URL scheme"):
|
||
validate_url_safety("example.com/file.mp3")
|
||
|
||
def test_http_scheme_allowed(self):
|
||
"""http scheme允许(需要mock DNS)."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("http://example.com/audio.mp3")
|
||
assert result == "http://example.com/audio.mp3"
|
||
mock_check.assert_called_once()
|
||
|
||
def test_https_scheme_allowed(self):
|
||
"""https scheme允许."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("https://example.com/audio.mp3")
|
||
assert result == "https://example.com/audio.mp3"
|
||
mock_check.assert_called_once()
|
||
|
||
def test_scheme_case_insensitive(self):
|
||
"""scheme大小写不敏感."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("HTTPS://example.com/audio.mp3")
|
||
assert result == "HTTPS://example.com/audio.mp3"
|
||
|
||
def test_missing_hostname_rejected(self):
|
||
"""缺少主机名被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="URL 缺少主机名"):
|
||
validate_url_safety("https:///path/to/file")
|
||
|
||
def test_non_standard_port_rejected(self):
|
||
"""非标准端口被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="不允许的端口"):
|
||
validate_url_safety("https://example.com:8080/audio.mp3")
|
||
|
||
def test_port_22_rejected(self):
|
||
"""SSH端口被拒绝."""
|
||
with pytest.raises(UrlSecurityError, match="不允许的端口"):
|
||
validate_url_safety("https://example.com:22/")
|
||
|
||
def test_port_80_allowed(self):
|
||
"""80端口允许."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("http://example.com:80/audio.mp3")
|
||
assert result == "http://example.com:80/audio.mp3"
|
||
|
||
def test_port_443_allowed(self):
|
||
"""443端口允许."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("https://example.com:443/audio.mp3")
|
||
assert result == "https://example.com:443/audio.mp3"
|
||
|
||
def test_default_http_port_implicit_80(self):
|
||
"""http默认端口隐含80,不触发端口校验."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("http://example.com/audio.mp3")
|
||
assert "example.com" in result
|
||
|
||
def test_default_https_port_implicit_443(self):
|
||
"""https默认端口隐含443,不触发端口校验."""
|
||
with patch("shared.url_security._check_ssrf_domain") as mock_check:
|
||
result = validate_url_safety("https://example.com/audio.mp3")
|
||
assert "example.com" in result
|
||
|
||
def test_purpose_parameter_logged(self):
|
||
"""purpose参数不影响校验结果."""
|
||
with patch("shared.url_security._check_ssrf_domain"):
|
||
result = validate_url_safety("https://example.com/a.mp3", purpose="tts_download")
|
||
assert result == "https://example.com/a.mp3"
|
||
|
||
def test_url_with_path_and_query(self):
|
||
"""带路径和query参数的URL正常通过."""
|
||
with patch("shared.url_security._check_ssrf_domain"):
|
||
url = "https://example.com/path/to/file.mp3?token=abc&expires=123"
|
||
result = validate_url_safety(url)
|
||
assert result == url
|
||
|
||
|
||
class TestInternalHostnames:
|
||
"""_check_internal_hostnames 内部主机名拦截."""
|
||
|
||
def test_localhost_blocked(self):
|
||
"""localhost被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="禁止访问内部主机名"):
|
||
_check_internal_hostnames("localhost")
|
||
|
||
def test_localhost_case_insensitive(self):
|
||
"""大小写不敏感."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_internal_hostnames("LOCALHOST")
|
||
|
||
def test_localhost_localdomain_blocked(self):
|
||
"""localhost.localdomain被拦截."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_internal_hostnames("localhost.localdomain")
|
||
|
||
def test_metadata_blocked(self):
|
||
"""metadata被拦截."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_internal_hostnames("metadata")
|
||
|
||
def test_metadata_google_internal_blocked(self):
|
||
"""GCP元数据服务被拦截."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_internal_hostnames("metadata.google.internal")
|
||
|
||
def test_local_domain_suffix_blocked(self):
|
||
""".local后缀被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="禁止访问内网域名"):
|
||
_check_internal_hostnames("printer.local")
|
||
|
||
def test_internal_domain_suffix_blocked(self):
|
||
""".internal后缀被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="禁止访问内网域名"):
|
||
_check_internal_hostnames("service.internal")
|
||
|
||
def test_localdomain_suffix_blocked(self):
|
||
""".localdomain后缀被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="禁止访问内网域名"):
|
||
_check_internal_hostnames("host.localdomain")
|
||
|
||
def test_public_domain_passes(self):
|
||
"""公网域名通过."""
|
||
_check_internal_hostnames("example.com") # 不抛异常即通过
|
||
|
||
def test_subdomain_of_public_domain_passes(self):
|
||
"""公网域名子域名通过."""
|
||
_check_internal_hostnames("cdn.example.com") # 不抛异常即通过
|
||
|
||
|
||
class TestSSRFIPCheck:
|
||
"""_check_ssrf_ip SSRF IP检查."""
|
||
|
||
def test_loopback_ipv4_blocked(self):
|
||
"""IPv4回环地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="回环地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("127.0.0.1"))
|
||
|
||
def test_loopback_ipv6_blocked(self):
|
||
"""IPv6回环地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="回环地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("::1"))
|
||
|
||
def test_private_ip_10_blocked(self):
|
||
"""10.x.x.x私有地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="内网地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("10.0.0.1"))
|
||
|
||
def test_private_ip_172_blocked(self):
|
||
"""172.16.x.x私有地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="内网地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("172.16.0.1"))
|
||
|
||
def test_private_ip_192_blocked(self):
|
||
"""192.168.x.x私有地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="内网地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("192.168.1.1"))
|
||
|
||
def test_link_local_blocked(self):
|
||
"""链路本地地址被拦截(is_private 先命中也可以,只要被拦就行)."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_ssrf_ip(ipaddress.ip_address("169.254.169.254"))
|
||
|
||
def test_multicast_blocked(self):
|
||
"""组播地址被拦截."""
|
||
with pytest.raises(UrlSecurityError, match="组播地址"):
|
||
_check_ssrf_ip(ipaddress.ip_address("224.0.0.1"))
|
||
|
||
def test_unspecified_ipv4_blocked(self):
|
||
"""0.0.0.0被拦截."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_ssrf_ip(ipaddress.ip_address("0.0.0.0"))
|
||
|
||
def test_unspecified_ipv6_blocked(self):
|
||
"""::被拦截."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_ssrf_ip(ipaddress.ip_address("::"))
|
||
|
||
def test_reserved_ip_blocked(self):
|
||
"""保留地址被拦截(240.0.0.0/4 属于保留段)."""
|
||
with pytest.raises(UrlSecurityError):
|
||
_check_ssrf_ip(ipaddress.ip_address("240.0.0.1"))
|
||
|
||
def test_public_ip_passes(self):
|
||
"""公网IP通过(当ALLOW_DIRECT_IP=true时)."""
|
||
# 注意:公网IP在_check_ssrf_ip层面不拦截,拦截在validate_url_safety的ALLOW_DIRECT_IP层
|
||
_check_ssrf_ip(ipaddress.ip_address("8.8.8.8")) # 不抛异常即通过
|
||
|
||
def test_public_ip_another_passes(self):
|
||
"""另一个公网IP通过."""
|
||
_check_ssrf_ip(ipaddress.ip_address("1.1.1.1")) # 不抛异常即通过
|
||
|
||
def test_ipv6_public_passes(self):
|
||
"""IPv6公网地址通过."""
|
||
_check_ssrf_ip(ipaddress.ip_address("2001:4860:4860::8888")) # 不抛异常即通过
|
||
|
||
|
||
class TestDirectIPBlocking:
|
||
"""直接IP访问拦截."""
|
||
|
||
def test_direct_ipv4_blocked_by_default(self):
|
||
"""默认禁止直接IP访问."""
|
||
with pytest.raises(UrlSecurityError, match="禁止直接 IP 访问"):
|
||
validate_url_safety("https://8.8.8.8/audio.mp3")
|
||
|
||
def test_direct_ipv6_blocked_by_default(self):
|
||
"""默认禁止直接IPv6访问."""
|
||
with pytest.raises(UrlSecurityError, match="禁止直接 IP 访问"):
|
||
validate_url_safety("https://[2001:4860:4860::8888]/audio.mp3")
|
||
|
||
def test_direct_ip_allowed_when_flag_set(self):
|
||
"""ALLOW_DIRECT_IP=true时允许公网IP直接访问."""
|
||
with patch("shared.url_security.ALLOW_DIRECT_IP", True):
|
||
# 公网IP应该通过
|
||
with patch("shared.url_security._check_ssrf_ip") as mock_check:
|
||
result = validate_url_safety("https://8.8.8.8/audio.mp3")
|
||
assert result == "https://8.8.8.8/audio.mp3"
|
||
mock_check.assert_called_once()
|
||
|
||
def test_direct_private_ip_blocked_even_with_flag(self):
|
||
"""即使ALLOW_DIRECT_IP=true,私有IP仍被SSRF检查拦截."""
|
||
with patch("shared.url_security.ALLOW_DIRECT_IP", True):
|
||
with pytest.raises(UrlSecurityError, match="内网地址"):
|
||
validate_url_safety("https://192.168.1.1/audio.mp3")
|
||
|
||
|
||
class TestTrustedDomain:
|
||
"""_is_trusted_domain 可信域名匹配."""
|
||
|
||
def test_exact_match(self):
|
||
"""精确匹配."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com"}):
|
||
assert _is_trusted_domain("example.com") is True
|
||
|
||
def test_subdomain_match(self):
|
||
"""子域名匹配."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com"}):
|
||
assert _is_trusted_domain("cdn.example.com") is True
|
||
|
||
def test_nested_subdomain_match(self):
|
||
"""多级子域名匹配."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com"}):
|
||
assert _is_trusted_domain("a.b.c.example.com") is True
|
||
|
||
def test_no_match(self):
|
||
"""不匹配的域名."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com"}):
|
||
assert _is_trusted_domain("other.com") is False
|
||
|
||
def test_domain_containing_but_not_subdomain(self):
|
||
"""域名包含但不是子域名."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com"}):
|
||
assert _is_trusted_domain("fake-example.com") is False
|
||
|
||
def test_case_insensitive(self):
|
||
"""大小写不敏感."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"Example.COM"}):
|
||
assert _is_trusted_domain("cdn.example.com") is True
|
||
|
||
def test_empty_trusted_domains(self):
|
||
"""空白名单时全部返回False."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", set()):
|
||
assert _is_trusted_domain("anything.com") is False
|
||
|
||
def test_multiple_trusted_domains(self):
|
||
"""多个可信域名."""
|
||
with patch("shared.url_security.TRUSTED_DOMAINS", {"example.com", "aliyuncs.com"}):
|
||
assert _is_trusted_domain("bucket.oss-cn-hangzhou.aliyuncs.com") is True
|
||
assert _is_trusted_domain("cdn.example.com") is True
|
||
assert _is_trusted_domain("unknown.com") is False
|
||
|
||
|
||
class TestIsUrlSafe:
|
||
"""is_url_safe 便捷函数."""
|
||
|
||
def test_safe_url_returns_true(self):
|
||
"""安全URL返回True."""
|
||
with patch("shared.url_security._check_ssrf_domain"):
|
||
assert is_url_safe("https://example.com/a.mp3") is True
|
||
|
||
def test_unsafe_url_returns_false(self):
|
||
"""不安全URL返回False."""
|
||
assert is_url_safe("ftp://example.com/a.mp3") is False
|
||
|
||
def test_empty_url_returns_false(self):
|
||
"""空URL返回False."""
|
||
assert is_url_safe("") is False
|
||
|
||
def test_does_not_raise(self):
|
||
"""不抛异常."""
|
||
try:
|
||
is_url_safe("not a url at all")
|
||
except Exception:
|
||
pytest.fail("is_url_safe should not raise exceptions")
|
||
|
||
|
||
class TestMagicNumberValidation:
|
||
"""_validate_magic_number 文件魔数校验."""
|
||
|
||
def test_valid_mp3_id3v2(self):
|
||
"""MP3 ID3v2标签魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
||
f.write(b"ID3" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"audio/mpeg"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_wav(self):
|
||
"""WAV文件魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
|
||
# RIFF + WAVE
|
||
f.write(b"RIFF" + b"\x00" * 4 + b"WAVE" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"audio/wav"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_png(self):
|
||
"""PNG图片魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
||
f.write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"image/png"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_jpeg(self):
|
||
"""JPEG图片魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f:
|
||
f.write(b"\xff\xd8\xff" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"image/jpeg"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_gif87a(self):
|
||
"""GIF87a通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as f:
|
||
f.write(b"GIF87a" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"image/gif"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_gif89a(self):
|
||
"""GIF89a通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".gif", delete=False) as f:
|
||
f.write(b"GIF89a" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"image/gif"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_valid_ogg(self):
|
||
"""OGG音频通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as f:
|
||
f.write(b"OggS" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"audio/ogg"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_invalid_magic_number_rejected(self):
|
||
"""错误魔数被拒绝."""
|
||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||
f.write(b"NOT_A_VALID_FILE" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
with pytest.raises(UrlSecurityError, match="文件魔数与允许的 MIME 类型不匹配"):
|
||
_validate_magic_number(tmp_path, {"audio/mpeg", "image/png"})
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_empty_file_rejected(self):
|
||
"""空文件被拒绝."""
|
||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||
tmp_path = f.name
|
||
try:
|
||
with pytest.raises(UrlSecurityError, match="文件为空"):
|
||
_validate_magic_number(tmp_path, {"audio/mpeg"})
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_unknown_mime_skipped(self):
|
||
"""未知MIME类型跳过校验."""
|
||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||
f.write(b"random data here")
|
||
tmp_path = f.name
|
||
try:
|
||
# 未知MIME类型没有对应的魔数,应该跳过不阻断
|
||
_validate_magic_number(tmp_path, {"application/x-unknown-type"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_empty_allowed_mime_skipped(self):
|
||
"""空allowed_mime_types集合直接通过(无魔数可比对)."""
|
||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||
f.write(b"anything")
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, set()) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_multiple_allowed_types_one_matches(self):
|
||
"""多个允许类型,只要一个匹配就通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
|
||
f.write(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"audio/mpeg", "image/png", "image/jpeg"})
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_mp4_ftyp_magic(self):
|
||
"""MP4 ftyp魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as f:
|
||
# ftyp在偏移4的位置
|
||
f.write(b"\x00\x00\x00\x20" + b"ftyp" + b"mp42" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"video/mp4"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_webp_magic(self):
|
||
"""WebP魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".webp", delete=False) as f:
|
||
f.write(b"RIFF" + b"\x00" * 4 + b"WEBP" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"image/webp"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_flac_magic(self):
|
||
"""FLAC魔数通过."""
|
||
with tempfile.NamedTemporaryFile(suffix=".flac", delete=False) as f:
|
||
f.write(b"fLaC" + b"\x00" * 100)
|
||
tmp_path = f.name
|
||
try:
|
||
_validate_magic_number(tmp_path, {"audio/flac"}) # 不抛异常即通过
|
||
finally:
|
||
os.unlink(tmp_path)
|
||
|
||
def test_file_too_short_for_signature(self):
|
||
"""文件太短,不够签名长度时不匹配."""
|
||
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
|
||
f.write(b"ID") # 只有2字节,不够ID3的3字节
|
||
tmp_path = f.name
|
||
try:
|
||
with pytest.raises(UrlSecurityError, match="文件魔数"):
|
||
_validate_magic_number(tmp_path, {"audio/mpeg"})
|
||
finally:
|
||
os.unlink(tmp_path)
|