公开数据看板、四位 Key 录入面板、管理后台与三级权限、动态字段配置、 PostgreSQL/SQLite 双支持、数据库备份,以及本版新增的成品图上传与预览。 图片相关: - 录入表单支持相册选图与手机端直接拍照,每条记录最多 6 张 - 浏览器内压缩到最长边 1600 并剥离 EXIF,入库统一为 JPG/PNG - 二进制直接入库,现有备份自动覆盖图片,部署无需新增卷 - 详情页缩略图宫格与全屏 lightbox,公开看板同样可见 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
114 lines
5.7 KiB
Python
114 lines
5.7 KiB
Python
"""四位 Key 录入面板的图片链路:这是手机端最主要的入口,必须整条走通。"""
|
|
import hashlib
|
|
import hmac
|
|
|
|
import pytest
|
|
|
|
from conftest import png_bytes, record_payload
|
|
|
|
|
|
@pytest.fixture()
|
|
def entry(client, admin):
|
|
"""用管理员账号配好四位 Key,再换成录入会话。"""
|
|
from app.config import settings
|
|
from app.main import ENTRY_COOKIE
|
|
|
|
admin_client, admin_headers = admin
|
|
response = admin_client.put("/api/me/entry-key", headers=admin_headers, json={"key": "1379", "password": settings.admin_password})
|
|
assert response.status_code == 200, response.text
|
|
client.cookies.clear()
|
|
assert client.post("/api/entry/auth", json={"key": "1379"}).status_code == 200
|
|
token = client.cookies.get(ENTRY_COOKIE)
|
|
csrf = hmac.new(settings.secret.encode(), f"entry:{token}".encode(), hashlib.sha256).hexdigest()
|
|
return client, {"X-CSRF-Token": csrf}
|
|
|
|
|
|
def entry_upload(client, headers, **kwargs):
|
|
files = {"file": ("field.png", png_bytes(48, 36), "image/png"), "thumbnail": ("t.png", png_bytes(12, 9), "image/png")}
|
|
return client.post("/api/entry/uploads/images", headers=headers, files=files, **kwargs)
|
|
|
|
|
|
def test_entry_upload_requires_the_entry_session(client):
|
|
client.cookies.clear()
|
|
assert client.post("/api/entry/uploads/images", files={"file": ("a.png", png_bytes(), "image/png")}).status_code == 401
|
|
|
|
|
|
def test_entry_upload_requires_the_entry_csrf_token(entry):
|
|
client, _ = entry
|
|
assert client.post("/api/entry/uploads/images", files={"file": ("a.png", png_bytes(), "image/png")}).status_code == 403
|
|
|
|
|
|
def test_admin_csrf_token_is_not_accepted_on_the_entry_endpoint(entry, admin):
|
|
"""两套会话的 CSRF 派生方式不同,管理端令牌不能拿来打录入接口。"""
|
|
client, _ = entry
|
|
_, admin_headers = admin
|
|
assert client.post("/api/entry/uploads/images", headers=admin_headers, files={"file": ("a.png", png_bytes(), "image/png")}).status_code == 403
|
|
|
|
|
|
def test_entry_record_is_saved_with_its_images(entry):
|
|
from app.config import settings
|
|
|
|
client, headers = entry
|
|
token = entry_upload(client, headers).json()["data"]["token"]
|
|
payload = record_payload("LAS-ENTRY-001", image_tokens=[token], confirm_username=settings.admin_username)
|
|
created = client.post("/api/entry/records", headers=headers, json=payload)
|
|
assert created.status_code == 200, created.text
|
|
|
|
images = client.get(f"/api/public/records/{created.json()['data']['id']}").json()["data"]["images"]
|
|
assert len(images) == 1 and images[0]["width"] == 48
|
|
|
|
|
|
def test_entry_record_rejects_a_mismatched_username(entry):
|
|
client, headers = entry
|
|
token = entry_upload(client, headers).json()["data"]["token"]
|
|
payload = record_payload("LAS-ENTRY-002", image_tokens=[token], confirm_username="someone-else")
|
|
assert client.post("/api/entry/records", headers=headers, json=payload).status_code == 403
|
|
|
|
|
|
def test_entry_pending_image_can_be_discarded(entry):
|
|
client, headers = entry
|
|
token = entry_upload(client, headers).json()["data"]["token"]
|
|
assert client.delete(f"/api/entry/uploads/images/{token}", headers=headers).status_code == 200
|
|
assert client.delete(f"/api/entry/uploads/images/{token}", headers=headers).status_code == 404
|
|
|
|
|
|
def test_pending_image_of_another_account_cannot_be_claimed(client, admin):
|
|
"""令牌必须同时校验归属,否则拿到别人的令牌就能把图挂到自己的记录上。"""
|
|
from app.config import settings
|
|
from app.main import SESSION_COOKIE
|
|
|
|
admin_client, admin_headers = admin
|
|
created = admin_client.post("/api/users", headers=admin_headers, json={"username": "uploader9", "display_name": "录入九", "password": "UploaderPass123", "role": "uploader"})
|
|
assert created.status_code in (200, 409)
|
|
|
|
# 换成另一个账号上传,拿到属于它的令牌
|
|
client.cookies.clear()
|
|
client.post("/api/auth/login", data={"username": "uploader9", "password": "UploaderPass123"})
|
|
other_csrf = hmac.new(settings.secret.encode(), client.cookies.get(SESSION_COOKIE).encode(), hashlib.sha256).hexdigest()
|
|
stolen = client.post("/api/uploads/images", headers={"X-CSRF-Token": other_csrf}, files={"file": ("mine.png", png_bytes(20, 20), "image/png")}).json()["data"]["token"]
|
|
|
|
# 换回管理员,用别人的令牌建记录必须失败
|
|
client.cookies.clear()
|
|
client.post("/api/auth/login", data={"username": settings.admin_username, "password": settings.admin_password})
|
|
admin_csrf = hmac.new(settings.secret.encode(), client.cookies.get(SESSION_COOKIE).encode(), hashlib.sha256).hexdigest()
|
|
response = client.post("/api/records", headers={"X-CSRF-Token": admin_csrf}, json=record_payload("LAS-STEAL-001", image_tokens=[stolen]))
|
|
assert response.status_code == 404
|
|
|
|
|
|
def test_thumbnail_variant_reports_its_own_media_type(entry):
|
|
from app.config import settings
|
|
|
|
client, headers = entry
|
|
body = client.post("/api/entry/uploads/images", headers=headers, files={
|
|
"file": ("photo.jpg", open("/System/Library/CoreServices/DefaultBackground.jpg", "rb").read() if False else png_bytes(64, 64), "image/png"),
|
|
"thumbnail": ("t.png", png_bytes(16, 16), "image/png"),
|
|
}).json()
|
|
token = body["data"]["token"]
|
|
payload = record_payload("LAS-ENTRY-004", image_tokens=[token], confirm_username=settings.admin_username)
|
|
record_id = client.post("/api/entry/records", headers=headers, json=payload).json()["data"]["id"]
|
|
image = client.get(f"/api/public/records/{record_id}").json()["data"]["images"][0]
|
|
assert "variant=thumb" in image["thumb_url"]
|
|
response = client.get(image["thumb_url"])
|
|
assert response.status_code == 200 and response.headers["content-type"] == "image/png"
|
|
assert response.headers["etag"].endswith('-thumb"')
|