138 lines
5.3 KiB
Python
138 lines
5.3 KiB
Python
|
|
"""系统设置(公司信息)接口测试。
|
|||
|
|
|
|||
|
|
守的是:
|
|||
|
|
- 单行语义:全系统恒一行,PUT 只改名称不增行
|
|||
|
|
- public 端点无鉴权可用但**白名单字段**——多一个敏感字段都是匿名泄漏
|
|||
|
|
- logo 上传三重校验(后缀/magic bytes/大小)——仅看后缀可被伪造
|
|||
|
|
- 审计留痕:系统设置属敏感配置,改必须有日志
|
|||
|
|
|
|||
|
|
真图片构造:PNG 用 8 字节签名 + 最小 IHDR;JPG 用 SOI 头 + 填充字节。
|
|||
|
|
仅校验 magic bytes 前缀,无需完整合法图片(解析由前端 img 完成)。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
import pytest
|
|||
|
|
from fastapi.testclient import TestClient
|
|||
|
|
|
|||
|
|
from app.main import app
|
|||
|
|
|
|||
|
|
pytestmark = pytest.mark.integration
|
|||
|
|
|
|||
|
|
_SETTINGS = "/api/v1/system/settings"
|
|||
|
|
|
|||
|
|
_PNG_BYTES = b"\x89PNG\r\n\x1a\n" + b"\x00" * 32
|
|||
|
|
_JPG_BYTES = b"\xff\xd8\xff\xe0" + b"\x00" * 32
|
|||
|
|
_TXT_BYTES = b"not an image at all"
|
|||
|
|
|
|||
|
|
|
|||
|
|
@pytest.fixture
|
|||
|
|
def client() -> TestClient:
|
|||
|
|
return TestClient(app)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestCompanyInfo:
|
|||
|
|
"""公司名称读取与更新。"""
|
|||
|
|
|
|||
|
|
def test_初始值与更新生效(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
got = client.get(_SETTINGS, headers=admin["headers"]).json()["data"]
|
|||
|
|
assert got["company_name"]
|
|||
|
|
# logo_url 允许为 None(未上传)或历史遗留的 URL——单行库可复用,
|
|||
|
|
# 断言不依赖「干净库」前提;上传行为由 TestLogoUpload 专类覆盖
|
|||
|
|
assert got["logo_url"] is None or "/logo/public" in got["logo_url"]
|
|||
|
|
|
|||
|
|
new_name = f"测试公司_{uuid.uuid4().hex[:6]}"
|
|||
|
|
put = client.put(_SETTINGS, json={"company_name": new_name}, headers=admin["headers"])
|
|||
|
|
assert put.status_code == 200, put.text
|
|||
|
|
assert put.json()["data"]["company_name"] == new_name
|
|||
|
|
assert (
|
|||
|
|
client.get(_SETTINGS, headers=admin["headers"]).json()["data"]["company_name"]
|
|||
|
|
== new_name
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_名称为空被拒(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
assert (
|
|||
|
|
client.put(_SETTINGS, json={"company_name": ""}, headers=admin["headers"]).status_code
|
|||
|
|
== 400
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_非管理员改名称被拒(self, client: TestClient, normal: dict) -> None:
|
|||
|
|
resp = client.put(_SETTINGS, json={"company_name": "越权公司"}, headers=normal["headers"])
|
|||
|
|
assert resp.status_code == 403
|
|||
|
|
|
|||
|
|
def test_修改留审计日志(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
client.put(
|
|||
|
|
_SETTINGS,
|
|||
|
|
json={"company_name": f"审计公司_{uuid.uuid4().hex[:6]}"},
|
|||
|
|
headers=admin["headers"],
|
|||
|
|
)
|
|||
|
|
logs = client.get(
|
|||
|
|
"/api/v1/system/operation-logs",
|
|||
|
|
params={"module": "系统设置"},
|
|||
|
|
headers=admin["headers"],
|
|||
|
|
).json()["data"]["list"]
|
|||
|
|
assert logs, "修改公司名称未产生操作日志"
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestPublicEndpoint:
|
|||
|
|
"""匿名端点:白名单字段 + 可用性。"""
|
|||
|
|
|
|||
|
|
def test_无令牌可读且仅白名单字段(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
name = f"公开公司_{uuid.uuid4().hex[:6]}"
|
|||
|
|
client.put(_SETTINGS, json={"company_name": name}, headers=admin["headers"])
|
|||
|
|
|
|||
|
|
resp = client.get(f"{_SETTINGS}/public")
|
|||
|
|
assert resp.status_code == 200
|
|||
|
|
data = resp.json()["data"]
|
|||
|
|
assert set(data) == {"company_name", "logo_url"}, "public 端点字段越界"
|
|||
|
|
assert data["company_name"] == name
|
|||
|
|
assert resp.headers.get("cache-control") is not None
|
|||
|
|
|
|||
|
|
def test_写操作匿名不可用(self, client: TestClient) -> None:
|
|||
|
|
assert client.put(f"{_SETTINGS}/public", json={"company_name": "x"}).status_code in {
|
|||
|
|
401,
|
|||
|
|
405,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestLogoUpload:
|
|||
|
|
"""logo 上传校验链。"""
|
|||
|
|
|
|||
|
|
def _upload(self, client: TestClient, admin: dict, filename: str, content: bytes):
|
|||
|
|
return client.post(
|
|||
|
|
f"{_SETTINGS}/logo",
|
|||
|
|
files={"file": (filename, content, "application/octet-stream")},
|
|||
|
|
headers=admin["headers"],
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def test_合法png上传成功且回读(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
resp = self._upload(client, admin, "logo.png", _PNG_BYTES)
|
|||
|
|
assert resp.status_code == 200, resp.text
|
|||
|
|
out = resp.json()["data"]
|
|||
|
|
assert out["logo_url"] and "/logo/public" in out["logo_url"]
|
|||
|
|
|
|||
|
|
img = client.get(f"{_SETTINGS}/logo/public")
|
|||
|
|
assert img.status_code == 200
|
|||
|
|
assert img.headers["content-type"] == "image/png"
|
|||
|
|
|
|||
|
|
def test_伪造扩展名被magic_bytes拦截(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
resp = self._upload(client, admin, "fake.png", _TXT_BYTES)
|
|||
|
|
assert resp.status_code == 400
|
|||
|
|
assert "不符" in resp.json()["message"]
|
|||
|
|
|
|||
|
|
def test_非法格式被拒(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
assert self._upload(client, admin, "x.gif", _PNG_BYTES).status_code == 400
|
|||
|
|
|
|||
|
|
def test_超限被拒(self, client: TestClient, admin: dict) -> None:
|
|||
|
|
big = _PNG_BYTES + b"\x00" * (2 * 1024 * 1024)
|
|||
|
|
resp = self._upload(client, admin, "big.png", big)
|
|||
|
|
assert resp.status_code == 400
|
|||
|
|
assert "2MB" in resp.json()["message"]
|
|||
|
|
|
|||
|
|
def test_匿名上传被拒(self, client: TestClient) -> None:
|
|||
|
|
resp = client.post(
|
|||
|
|
f"{_SETTINGS}/logo",
|
|||
|
|
files={"file": ("logo.png", _PNG_BYTES, "application/octet-stream")},
|
|||
|
|
)
|
|||
|
|
assert resp.status_code == 401
|