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
453 lines
17 KiB
Python
Executable File
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"
|