fix(ci): auto-fix lint/format issues
PR Automation / Auto Approve on CI Green (pull_request) Successful in 59s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m1s
AI Code Review / AI Code Review (pull_request) Successful in 8m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 23m9s
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 26s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m19s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1343h32m8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1343h34m8s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1343h34m36s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1343h34m37s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1343h34m39s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1343h35m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1343h35m52s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1343h35m55s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1343h35m57s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1344h9m29s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1344h9m33s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1344h41m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1344h6m53s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 59s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m1s
AI Code Review / AI Code Review (pull_request) Successful in 8m38s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 23m9s
CI/CD Pipeline / Check if frontend-only change (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 26s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 47s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m19s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1343h32m8s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1343h34m8s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1343h34m36s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1343h34m37s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1343h34m39s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1343h35m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1343h35m52s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1343h35m55s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1343h35m57s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1344h9m29s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1344h9m33s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1344h41m46s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1344h6m53s
This commit is contained in:
@@ -11,28 +11,27 @@ url_security URL安全校验单元测试
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from packages.shared.url_security import (
|
||||
ALLOWED_SCHEMES,
|
||||
ALLOWED_PORTS,
|
||||
MAX_URL_LENGTH,
|
||||
UrlSecurityError,
|
||||
NoRedirectHandler,
|
||||
validate_url_safety,
|
||||
is_url_safe,
|
||||
_validate_magic_number,
|
||||
_check_internal_hostnames,
|
||||
_is_trusted_domain,
|
||||
safe_download_file,
|
||||
safe_download_bytes,
|
||||
ALLOWED_AUDIO_MIME_TYPES,
|
||||
ALLOWED_IMAGE_MIME_TYPES,
|
||||
ALLOWED_PORTS,
|
||||
ALLOWED_SCHEMES,
|
||||
MAX_URL_LENGTH,
|
||||
NoRedirectHandler,
|
||||
UrlSecurityError,
|
||||
_check_internal_hostnames,
|
||||
_is_trusted_domain,
|
||||
_validate_magic_number,
|
||||
is_url_safe,
|
||||
safe_download_bytes,
|
||||
safe_download_file,
|
||||
validate_url_safety,
|
||||
)
|
||||
|
||||
|
||||
# ── validate_url_safety 基础校验 ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -203,9 +202,10 @@ class TestTrustedDomains:
|
||||
def test_is_trusted_domain_exact_match(self):
|
||||
with patch("packages.shared.url_security.TRUSTED_DOMAINS", {"example.com", "cdn.example.org"}):
|
||||
# 重新加载模块以应用环境变量不太现实,直接测函数
|
||||
from packages.shared.url_security import _is_trusted_domain
|
||||
# 直接改全局状态再还原
|
||||
import packages.shared.url_security as mod
|
||||
from packages.shared.url_security import _is_trusted_domain
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com", "cdn.example.org"}
|
||||
try:
|
||||
@@ -216,6 +216,7 @@ class TestTrustedDomains:
|
||||
|
||||
def test_is_trusted_domain_subdomain(self):
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com"}
|
||||
try:
|
||||
@@ -226,6 +227,7 @@ class TestTrustedDomains:
|
||||
|
||||
def test_is_trusted_domain_no_match(self):
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"example.com"}
|
||||
try:
|
||||
@@ -237,6 +239,7 @@ class TestTrustedDomains:
|
||||
def test_validate_with_trusted_domains_restricted(self):
|
||||
"""白名单非空时,不在白名单中的域名被拒"""
|
||||
import packages.shared.url_security as mod
|
||||
|
||||
original = mod.TRUSTED_DOMAINS
|
||||
mod.TRUSTED_DOMAINS = {"trusted.com"}
|
||||
try:
|
||||
@@ -297,8 +300,12 @@ class TestNoRedirectHandler:
|
||||
def test_redirect_request_returns_none(self):
|
||||
handler = NoRedirectHandler()
|
||||
result = handler.redirect_request(
|
||||
MagicMock(), MagicMock(), 302, "Found",
|
||||
{"Location": "http://other.com"}, "http://other.com",
|
||||
MagicMock(),
|
||||
MagicMock(),
|
||||
302,
|
||||
"Found",
|
||||
{"Location": "http://other.com"},
|
||||
"http://other.com",
|
||||
)
|
||||
assert result is None
|
||||
|
||||
@@ -312,80 +319,80 @@ class TestMagicNumberValidation:
|
||||
def test_valid_png(self, tmp_path):
|
||||
f = tmp_path / "test.png"
|
||||
# PNG 文件头: 89 50 4E 47 0D 0A 1A 0A
|
||||
f.write_bytes(b'\x89PNG\r\n\x1a\n' + b'\x00' * 100)
|
||||
f.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 100)
|
||||
# 不抛异常 = 通过
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_jpeg(self, tmp_path):
|
||||
f = tmp_path / "test.jpg"
|
||||
# JPEG 文件头: FF D8 FF
|
||||
f.write_bytes(b'\xff\xd8\xff\xe0' + b'\x00' * 100)
|
||||
f.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_gif87a(self, tmp_path):
|
||||
f = tmp_path / "test.gif"
|
||||
f.write_bytes(b'GIF87a' + b'\x00' * 100)
|
||||
f.write_bytes(b"GIF87a" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_gif89a(self, tmp_path):
|
||||
f = tmp_path / "test.gif"
|
||||
f.write_bytes(b'GIF89a' + b'\x00' * 100)
|
||||
f.write_bytes(b"GIF89a" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_webp(self, tmp_path):
|
||||
f = tmp_path / "test.webp"
|
||||
# RIFF....WEBP
|
||||
data = bytearray(b'RIFF')
|
||||
data += b'\x00\x00\x00\x00' # size placeholder
|
||||
data += b'WEBP'
|
||||
data += b'\x00' * 100
|
||||
data = bytearray(b"RIFF")
|
||||
data += b"\x00\x00\x00\x00" # size placeholder
|
||||
data += b"WEBP"
|
||||
data += b"\x00" * 100
|
||||
f.write_bytes(bytes(data))
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_bmp(self, tmp_path):
|
||||
f = tmp_path / "test.bmp"
|
||||
f.write_bytes(b'BM' + b'\x00' * 100)
|
||||
f.write_bytes(b"BM" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_valid_wav(self, tmp_path):
|
||||
f = tmp_path / "test.wav"
|
||||
# RIFF....WAVE
|
||||
data = bytearray(b'RIFF')
|
||||
data += b'\x00\x00\x00\x00'
|
||||
data += b'WAVE'
|
||||
data += b'\x00' * 100
|
||||
data = bytearray(b"RIFF")
|
||||
data += b"\x00\x00\x00\x00"
|
||||
data += b"WAVE"
|
||||
data += b"\x00" * 100
|
||||
f.write_bytes(bytes(data))
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_mp3_id3(self, tmp_path):
|
||||
f = tmp_path / "test.mp3"
|
||||
f.write_bytes(b'ID3\x03\x00\x00\x00\x00\x00\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"ID3\x03\x00\x00\x00\x00\x00\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_mp3_adts(self, tmp_path):
|
||||
f = tmp_path / "test.mp3"
|
||||
f.write_bytes(b'\xff\xfb\x90\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"\xff\xfb\x90\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_ogg(self, tmp_path):
|
||||
f = tmp_path / "test.ogg"
|
||||
f.write_bytes(b'OggS\x00\x02\x00\x00' + b'\x00' * 100)
|
||||
f.write_bytes(b"OggS\x00\x02\x00\x00" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_valid_flac(self, tmp_path):
|
||||
f = tmp_path / "test.flac"
|
||||
f.write_bytes(b'fLaC' + b'\x00' * 100)
|
||||
f.write_bytes(b"fLaC" + b"\x00" * 100)
|
||||
_validate_magic_number(str(f), ALLOWED_AUDIO_MIME_TYPES)
|
||||
|
||||
def test_invalid_file_content_raises(self, tmp_path):
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'this is not an image file at all')
|
||||
f.write_bytes(b"this is not an image file at all")
|
||||
with pytest.raises(UrlSecurityError, match="魔数"):
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
def test_empty_file_raises(self, tmp_path):
|
||||
f = tmp_path / "empty.bin"
|
||||
f.write_bytes(b'')
|
||||
f.write_bytes(b"")
|
||||
with pytest.raises(UrlSecurityError, match="为空"):
|
||||
_validate_magic_number(str(f), ALLOWED_IMAGE_MIME_TYPES)
|
||||
|
||||
@@ -396,14 +403,14 @@ class TestMagicNumberValidation:
|
||||
def test_no_allowed_mime_types_skips(self, tmp_path):
|
||||
"""allowed_mime_types 为空时跳过校验"""
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'random data here')
|
||||
f.write_bytes(b"random data here")
|
||||
# 不抛异常
|
||||
_validate_magic_number(str(f), set())
|
||||
|
||||
def test_unknown_mime_types_skips(self, tmp_path):
|
||||
"""没有已知魔数的 MIME 类型跳过校验"""
|
||||
f = tmp_path / "test.bin"
|
||||
f.write_bytes(b'random data')
|
||||
f.write_bytes(b"random data")
|
||||
_validate_magic_number(str(f), {"application/x-unknown-type"})
|
||||
|
||||
|
||||
@@ -441,7 +448,7 @@ class TestSafeDownloadFile:
|
||||
|
||||
def test_download_with_mime_check_passes(self, tmp_path):
|
||||
# PNG 文件
|
||||
test_content = b'\x89PNG\r\n\x1a\n' + b'\x00' * 200
|
||||
test_content = b"\x89PNG\r\n\x1a\n" + b"\x00" * 200
|
||||
dest = str(tmp_path / "test.png")
|
||||
|
||||
mock_resp = MagicMock()
|
||||
@@ -463,7 +470,7 @@ class TestSafeDownloadFile:
|
||||
assert size == len(test_content)
|
||||
|
||||
def test_download_mime_type_rejected(self, tmp_path):
|
||||
test_content = b'GIF89a' + b'\x00' * 50
|
||||
test_content = b"GIF89a" + b"\x00" * 50
|
||||
dest = str(tmp_path / "test.gif")
|
||||
|
||||
mock_resp = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user