Files
xiaoxia-saas/tests/unit/test_watermark_config.py
xiaoxia d537dd2ed0
CI/CD Pipeline / Check if frontend-only change (push) Has been cancelled
CI/CD Pipeline / Validate - Code Quality (push) Has been cancelled
CI/CD Pipeline / Validate - Type Check (mypy) (push) Has been cancelled
CI/CD Pipeline / Validate - Migration (alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Frontend Lint (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / PR Build API Image (push) Has been cancelled
CI/CD Pipeline / PR Build Web Image (push) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
test(wave115): 抽离watermark_config领域模型 + 76单测 (#1001)
2026-07-27 08:44:53 +08:00

453 lines
17 KiB
Python
Executable File

"""watermark_config 领域模型单测."""
from __future__ import annotations
import pytest
from packages.domain.watermark_config import (
DEFAULT_FONT_COLOR,
DEFAULT_FONT_SIZE,
DEFAULT_MODE,
DEFAULT_OPACITY,
DEFAULT_POSITION,
DEFAULT_SCALE,
VALID_POSITIONS,
WATERMARK_POSITIONS,
WatermarkConfig,
build_image_watermark_filter,
build_text_watermark_filter,
calc_position,
calc_scroll_x,
get_position_display_name,
get_position_names,
)
# ── 常量测试 ────────────────────────────────────────────────────────────────
class TestConstants:
def test_nine_positions(self):
assert len(WATERMARK_POSITIONS) == 9
def test_all_position_keys_valid(self):
for key in WATERMARK_POSITIONS:
assert key in VALID_POSITIONS
def test_valid_positions_match(self):
assert set(WATERMARK_POSITIONS.keys()) == VALID_POSITIONS
def test_default_values(self):
assert DEFAULT_POSITION == "bottom_right"
assert DEFAULT_MODE == "text"
assert DEFAULT_SCALE == 0.2
assert DEFAULT_OPACITY == 0.8
assert DEFAULT_FONT_SIZE == 24
assert DEFAULT_FONT_COLOR == "white"
# ── WatermarkConfig.from_dict 测试 ─────────────────────────────────────────
class TestWatermarkConfigFromDict:
def test_none_returns_none(self):
assert WatermarkConfig.from_dict(None) is None
def test_empty_dict_returns_none(self):
assert WatermarkConfig.from_dict({}) is None
def test_disabled_returns_none(self):
assert WatermarkConfig.from_dict({"enabled": False}) is None
def test_text_mode_basic(self):
cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hello"})
assert cfg is not None
assert cfg.mode == "text"
assert cfg.text == "hello"
assert cfg.position == DEFAULT_POSITION
def test_image_mode_basic(self):
cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image_path": "/tmp/wm.png"})
assert cfg is not None
assert cfg.mode == "image"
assert cfg.image_path == "/tmp/wm.png"
def test_image_mode_accepts_image_key(self):
cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "image", "image": "/tmp/wm.png"})
assert cfg is not None
assert cfg.image_path == "/tmp/wm.png"
def test_image_mode_missing_path_returns_none(self):
assert WatermarkConfig.from_dict({"enabled": True, "mode": "image"}) is None
def test_text_mode_missing_text_returns_none(self):
assert WatermarkConfig.from_dict({"enabled": True, "mode": "text"}) is None
def test_text_mode_empty_text_returns_none(self):
assert WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": ""}) is None
def test_invalid_position_defaults(self):
cfg = WatermarkConfig.from_dict({"enabled": True, "mode": "text", "text": "hi", "position": "invalid"})
assert cfg.position == DEFAULT_POSITION
def test_custom_all_params(self):
cfg = WatermarkConfig.from_dict(
{
"enabled": True,
"mode": "text",
"text": "测试水印",
"position": "top_left",
"font_size": 32,
"font_color": "red",
"opacity": 0.5,
"margin_x": 30,
"margin_y": 40,
"scroll": True,
"scroll_speed": 100,
}
)
assert cfg is not None
assert cfg.text == "测试水印"
assert cfg.position == "top_left"
assert cfg.font_size == 32
assert cfg.font_color == "red"
assert cfg.opacity == 0.5
assert cfg.margin_x == 30
assert cfg.margin_y == 40
assert cfg.scroll is True
assert cfg.scroll_speed == 100
def test_default_mode_is_text(self):
cfg = WatermarkConfig.from_dict({"enabled": True, "text": "hi"})
assert cfg is not None
assert cfg.mode == "text"
# ── WatermarkConfig.validate 测试 ──────────────────────────────────────────
class TestWatermarkConfigValidate:
def test_valid_text_config(self):
cfg = WatermarkConfig(mode="text", text="hello")
ok, msg = cfg.validate()
assert ok is True
assert msg == ""
def test_valid_image_config(self):
cfg = WatermarkConfig(mode="image", image_path="/tmp/wm.png", scale=0.3)
ok, msg = cfg.validate()
assert ok is True
def test_invalid_position(self):
cfg = WatermarkConfig(mode="text", text="hi", position="nowhere")
ok, msg = cfg.validate()
assert ok is False
assert "位置" in msg
def test_opacity_negative(self):
cfg = WatermarkConfig(mode="text", text="hi", opacity=-0.1)
ok, msg = cfg.validate()
assert ok is False
assert "透明度" in msg
def test_opacity_over_one(self):
cfg = WatermarkConfig(mode="text", text="hi", opacity=1.5)
ok, msg = cfg.validate()
assert ok is False
def test_opacity_boundary_zero(self):
cfg = WatermarkConfig(mode="text", text="hi", opacity=0.0)
ok, _ = cfg.validate()
assert ok is True
def test_opacity_boundary_one(self):
cfg = WatermarkConfig(mode="text", text="hi", opacity=1.0)
ok, _ = cfg.validate()
assert ok is True
def test_image_missing_path(self):
cfg = WatermarkConfig(mode="image")
ok, msg = cfg.validate()
assert ok is False
assert "图片路径" in msg
def test_image_scale_too_small(self):
cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=0.001)
ok, msg = cfg.validate()
assert ok is False
assert "缩放比例" in msg
def test_image_scale_too_large(self):
cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=2.0)
ok, msg = cfg.validate()
assert ok is False
def test_image_scale_boundary_low(self):
cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=0.01)
ok, _ = cfg.validate()
assert ok is True
def test_image_scale_boundary_high(self):
cfg = WatermarkConfig(mode="image", image_path="/a.png", scale=1.0)
ok, _ = cfg.validate()
assert ok is True
def test_text_missing_content(self):
cfg = WatermarkConfig(mode="text", text="")
ok, msg = cfg.validate()
assert ok is False
assert "文字内容" in msg
def test_text_font_size_zero(self):
cfg = WatermarkConfig(mode="text", text="hi", font_size=0)
ok, msg = cfg.validate()
assert ok is False
assert "字体大小" in msg
def test_text_font_size_negative(self):
cfg = WatermarkConfig(mode="text", text="hi", font_size=-5)
ok, msg = cfg.validate()
assert ok is False
def test_unknown_mode(self):
cfg = WatermarkConfig(mode="video")
ok, msg = cfg.validate()
assert ok is False
assert "模式" in msg
# ── has_effect 测试 ────────────────────────────────────────────────────────
class TestHasEffect:
def test_text_with_content_has_effect(self):
cfg = WatermarkConfig(mode="text", text="hello")
assert cfg.has_effect() is True
def test_text_empty_no_effect(self):
cfg = WatermarkConfig(mode="text", text="")
assert cfg.has_effect() is False
def test_text_zero_opacity_no_effect(self):
cfg = WatermarkConfig(mode="text", text="hello", opacity=0.0)
assert cfg.has_effect() is False
def test_image_with_path_has_effect(self):
cfg = WatermarkConfig(mode="image", image_path="/a.png")
assert cfg.has_effect() is True
def test_image_no_path_no_effect(self):
cfg = WatermarkConfig(mode="image")
assert cfg.has_effect() is False
def test_unknown_mode_no_effect(self):
cfg = WatermarkConfig(mode="unknown")
assert cfg.has_effect() is False
# ── calc_position 测试 ─────────────────────────────────────────────────────
class TestCalcPosition:
def test_top_left(self):
x, y = calc_position("top_left", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (10, 20)
def test_top_center(self):
x, y = calc_position("top_center", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (450, 20)
def test_top_right(self):
x, y = calc_position("top_right", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (890, 20)
def test_center_left(self):
x, y = calc_position("center_left", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (10, 975)
def test_center(self):
x, y = calc_position("center", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (450, 975)
def test_center_right(self):
x, y = calc_position("center_right", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (890, 975)
def test_bottom_left(self):
x, y = calc_position("bottom_left", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (10, 1930)
def test_bottom_center(self):
x, y = calc_position("bottom_center", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (450, 1930)
def test_bottom_right(self):
x, y = calc_position("bottom_right", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (890, 1930)
def test_unknown_position_defaults_bottom_right(self):
x, y = calc_position("invalid", 1000, 2000, 100, 50, 10, 20)
assert (x, y) == (890, 1930)
def test_zero_margin(self):
x, y = calc_position("top_left", 1000, 2000, 100, 50, 0, 0)
assert (x, y) == (0, 0)
def test_small_output(self):
x, y = calc_position("center", 100, 100, 50, 30, 5, 5)
assert (x, y) == (25, 35)
# ── calc_scroll_x 测试 ─────────────────────────────────────────────────────
class TestCalcScrollX:
def test_returns_string_expression(self):
result = calc_scroll_x("bottom", 1000, 200, 50)
assert isinstance(result, str)
def test_contains_mod_function(self):
result = calc_scroll_x("bottom", 1000, 200, 50)
assert "mod" in result
def test_contains_speed_and_width(self):
result = calc_scroll_x("bottom", 1080, 300, 60)
assert "1080" in result
assert "60" in result
assert "300" in result
# ── build_image_watermark_filter 测试 ──────────────────────────────────────
class TestBuildImageWatermarkFilter:
def test_returns_tuple(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png")
result = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert isinstance(result, tuple)
assert len(result) == 2
def test_filter_contains_overlay(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png")
filter_str, inputs = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert "overlay" in filter_str
def test_filter_contains_scale(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png", scale=0.5)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert "scale=" in filter_str
def test_full_opacity_no_alpha_filter(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png", opacity=1.0)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert "colorchannelmixer" not in filter_str
def test_partial_opacity_has_alpha_filter(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png", opacity=0.5)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert "colorchannelmixer" in filter_str
assert "aa=0.5" in filter_str
def test_input_args_contains_image_path(self):
cfg = WatermarkConfig(mode="image", image_path="/path/to/wm.png")
_, inputs = build_image_watermark_filter("[in]", "/path/to/wm.png", 1080, 1920, "[out]", cfg)
assert inputs == ["-i", "/path/to/wm.png"]
def test_scroll_mode_has_t_variable(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png", scroll=True, scroll_speed=50)
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[out]", cfg)
assert "t" in filter_str
def test_output_label_appears(self):
cfg = WatermarkConfig(mode="image", image_path="/wm.png")
filter_str, _ = build_image_watermark_filter("[in]", "/wm.png", 1080, 1920, "[final]", cfg)
assert "[final]" in filter_str
# ── build_text_watermark_filter 测试 ───────────────────────────────────────
class TestBuildTextWatermarkFilter:
def test_returns_string(self):
cfg = WatermarkConfig(mode="text", text="hello")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert isinstance(result, str)
def test_contains_drawtext(self):
cfg = WatermarkConfig(mode="text", text="hello")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "drawtext=" in result
def test_contains_text_content(self):
cfg = WatermarkConfig(mode="text", text="watermark_test")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "watermark_test" in result
def test_contains_font_size(self):
cfg = WatermarkConfig(mode="text", text="hi", font_size=48)
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "fontsize=48" in result
def test_contains_font_color(self):
cfg = WatermarkConfig(mode="text", text="hi", font_color="red")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "fontcolor=red" in result
def test_font_path_included_when_set(self):
cfg = WatermarkConfig(mode="text", text="hi", font_path="/fonts/a.ttf")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "fontfile=" in result
assert "a.ttf" in result
def test_font_path_not_included_when_empty(self):
cfg = WatermarkConfig(mode="text", text="hi")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "fontfile=" not in result
def test_scroll_mode_has_t_variable(self):
cfg = WatermarkConfig(mode="text", text="hello", scroll=True, scroll_speed=30)
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "t" in result
def test_no_scroll_uses_fixed_position(self):
cfg = WatermarkConfig(mode="text", text="hello", scroll=False)
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
assert "x=" in result
# 非滚动模式 x= 后面应该是数字,不是表达式
# 找 x= 后的第一个字符
import re
match = re.search(r"x=(\d+)", result)
assert match is not None
def test_output_label_appears(self):
cfg = WatermarkConfig(mode="text", text="hi")
result = build_text_watermark_filter("[in]", "[text_out]", cfg, 1080, 1920)
assert "[text_out]" in result
def test_special_chars_escaped(self):
cfg = WatermarkConfig(mode="text", text="hello:world")
result = build_text_watermark_filter("[in]", "[out]", cfg, 1080, 1920)
# 冒号应该被转义
assert "hello\\:world" in result or "hello\\\\\\:world" in result or "hello\\:" in result
# ── 工具函数测试 ────────────────────────────────────────────────────────────
class TestUtils:
def test_get_position_names_returns_nine(self):
names = get_position_names()
assert len(names) == 9
def test_get_position_names_all_valid(self):
names = get_position_names()
for name in names:
assert name in VALID_POSITIONS
def test_get_position_display_name_valid(self):
assert get_position_display_name("top_left") == "左上"
assert get_position_display_name("bottom_right") == "右下"
def test_get_position_display_name_invalid(self):
assert get_position_display_name("invalid") == "invalid"