master/develop 共用 release 轨(App_Store 打包构建号自增),feature 分支走
feature 轨(构建号永不自增,始终使用手填值),两条轨道的 App_Ver 各自独立。
- versions 改为 tracks 结构,旧的单轨/单值配置在加载时自动迁移进 release 轨
- 新增 config["branch_track"] 记录分支归属,缺省按分支名推断,可在分支管理页改
- 新增 PUT /api/config/branches/track;分支名含 / 时走请求体而非路径参数
- 修复 DELETE /api/config/branches/{name} 无法删除含 / 的分支(405)
- 版本号接口不再对普通用户开放,打包设置页整页改为管理员可见
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
71 lines
2.9 KiB
Python
71 lines
2.9 KiB
Python
"""公网部署安全回归测试。"""
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from starlette.websockets import WebSocketDisconnect
|
|
|
|
from backend import config
|
|
from backend.main import app
|
|
from backend.security import hash_password, verify_password
|
|
|
|
|
|
def test_passwords_use_bcrypt_and_verify():
|
|
password_hash = hash_password("a-strong-password")
|
|
|
|
assert password_hash.startswith("$2")
|
|
assert verify_password("a-strong-password", password_hash) == (True, False)
|
|
assert verify_password("wrong-password", password_hash) == (False, False)
|
|
|
|
|
|
def test_websocket_rejects_connection_without_jwt():
|
|
with TestClient(app) as unauthenticated_client:
|
|
with pytest.raises(WebSocketDisconnect) as exc_info:
|
|
with unauthenticated_client.websocket_connect("/ws/tasks/task-1"):
|
|
pass
|
|
|
|
assert exc_info.value.code == 1008
|
|
|
|
|
|
def test_production_rejects_default_security_settings(monkeypatch):
|
|
monkeypatch.setattr(config, "APP_ENV", "production")
|
|
monkeypatch.setattr(config, "JWT_SECRET", "ios-build-server-secret-key-change-in-production")
|
|
monkeypatch.setattr(config, "ADMIN_PASSWORD", "admin123")
|
|
monkeypatch.setattr(config, "CORS_ALLOWED_ORIGINS", ["*"])
|
|
monkeypatch.setattr(config, "TRUSTED_HOSTS", ["*"])
|
|
monkeypatch.setenv("TRUSTED_HOSTS", "*")
|
|
|
|
with pytest.raises(RuntimeError, match="生产环境安全配置不完整"):
|
|
config.validate_production_security()
|
|
|
|
|
|
def test_config_routes_limit_regular_users_to_apps(client, tmp_config):
|
|
"""普通账号只能管理 Apps,版本号等其余配置均为管理员专属。"""
|
|
response = client.post("/api/users", json={
|
|
"username": "builder",
|
|
"password": "builder-password-123",
|
|
"is_admin": False,
|
|
})
|
|
assert response.status_code == 200
|
|
|
|
login = client.post("/api/auth/login", json={
|
|
"username": "builder",
|
|
"password": "builder-password-123",
|
|
})
|
|
token = login.json()["token"]
|
|
headers = {"Authorization": f"Bearer {token}"}
|
|
|
|
assert client.get("/api/config/apps", headers=headers).status_code == 200
|
|
assert client.post("/api/config/apps", json={
|
|
"name": "普通用户 App", "server": "测试环境",
|
|
}, headers=headers).status_code == 200
|
|
assert client.get("/api/config/versions", headers=headers).status_code == 403
|
|
assert client.put(
|
|
"/api/config/versions", json={"app_ver": "2.196.0"}, headers=headers
|
|
).status_code == 403
|
|
assert client.put(
|
|
"/api/config/branches/track", json={"branch": "master", "track": "feature"}, headers=headers
|
|
).status_code == 403
|
|
assert client.get("/api/config", headers=headers).status_code == 403
|
|
assert client.get("/api/config/servers", headers=headers).status_code == 403
|
|
assert client.put("/api/config/build", json={"max_concurrent_builds": 1}, headers=headers).status_code == 403
|
|
assert client.put("/api/config/upload", json={"mode": "oss"}, headers=headers).status_code == 403
|