- 分支管理新增「分支 / Tag」类型选择,Tag 存于 config["tags"],与分支不可重名 - 每个 Tag 与分支一样拥有同名独立版本轨道,默认上架轨 - 打包页按「分支」「Tag」分组选择;任务新增 ref_type 字段记录引用类型 - Tag 打包时 fetch --tags --force 后以游离 HEAD 检出 refs/tags/<tag> - 历史记录与下载页按类型显示「分支」或「Tag」 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
382 lines
16 KiB
Python
382 lines
16 KiB
Python
"""配置管理接口测试"""
|
|
import json
|
|
|
|
|
|
def test_get_full_config(client, tmp_config):
|
|
resp = client.get("/api/config")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "apps" in data
|
|
assert "schemes" in data
|
|
assert "branches" in data
|
|
|
|
|
|
# ---- Apps ----
|
|
|
|
def test_get_apps(client, tmp_config):
|
|
resp = client.get("/api/config/apps")
|
|
assert resp.status_code == 200
|
|
assert "1" in resp.json()
|
|
|
|
|
|
def test_create_app(client, tmp_config):
|
|
resp = client.post("/api/config/apps", json={
|
|
"name": "新App", "server": "测试环境", "AppGuid": "new-guid",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["id"] == "100"
|
|
|
|
# 验证已创建
|
|
apps = client.get("/api/config/apps").json()
|
|
assert "100" in apps
|
|
assert apps["100"]["name"] == "新App"
|
|
|
|
|
|
def test_create_app_uses_environment_prefix_and_dictionary_exception(client, tmp_config):
|
|
official = client.post("/api/config/apps", json={"name": "正式 App", "server": "正式环境"})
|
|
dictionary = client.post("/api/config/apps", json={"name": "英汉大词典", "server": "正式环境"})
|
|
|
|
assert official.status_code == 200
|
|
assert official.json()["id"] == "200"
|
|
assert dictionary.status_code == 200
|
|
assert dictionary.json()["id"] == "100"
|
|
|
|
|
|
def test_create_app_rejects_unknown_environment_id_rule(client, tmp_config):
|
|
resp = client.post("/api/config/apps", json={"name": "新 App", "server": "未知环境"})
|
|
assert resp.status_code == 400
|
|
assert "ID 规则" in resp.json()["detail"]
|
|
|
|
|
|
def test_create_app_prefixes_tencent_app_id(client, tmp_config):
|
|
resp = client.post("/api/config/apps", json={
|
|
"name": "腾讯App", "server": "测试环境", "AppGuid": "guid", "tencent": "123456",
|
|
})
|
|
assert resp.status_code == 200
|
|
app_id = resp.json()["id"]
|
|
assert client.get("/api/config/apps").json()[app_id]["tencent"] == "tencent123456"
|
|
|
|
|
|
def test_create_app_keeps_existing_tencent_prefix(client, tmp_config):
|
|
resp = client.post("/api/config/apps", json={
|
|
"name": "腾讯App2", "server": "测试环境", "AppGuid": "guid", "tencent": "tencent123456",
|
|
})
|
|
app_id = resp.json()["id"]
|
|
assert client.get("/api/config/apps").json()[app_id]["tencent"] == "tencent123456"
|
|
|
|
|
|
def test_update_app_prefixes_tencent_app_id(client, tmp_config):
|
|
resp = client.put("/api/config/apps/1", json={
|
|
"name": "改名App", "AppGuid": "test-guid", "tencent": " 987654 ",
|
|
})
|
|
assert resp.status_code == 200
|
|
assert client.get("/api/config/apps").json()["1"]["tencent"] == "tencent987654"
|
|
|
|
|
|
def test_update_app(client, tmp_config):
|
|
resp = client.put("/api/config/apps/1", json={"name": "改名App", "AppGuid": "test-guid"})
|
|
assert resp.status_code == 200
|
|
assert client.get("/api/config/apps").json()["1"]["name"] == "改名App"
|
|
|
|
|
|
def test_delete_app(client, tmp_config):
|
|
resp = client.delete("/api/config/apps/1")
|
|
assert resp.status_code == 200
|
|
assert "1" not in client.get("/api/config/apps").json()
|
|
|
|
|
|
def test_update_nonexistent_app(client, tmp_config):
|
|
resp = client.put("/api/config/apps/999", json={"name": "x"})
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---- Schemes ----
|
|
|
|
def test_get_schemes(client, tmp_config):
|
|
resp = client.get("/api/config/schemes")
|
|
assert resp.status_code == 200
|
|
assert "1" in resp.json()
|
|
|
|
|
|
def test_create_scheme(client, tmp_config):
|
|
resp = client.post("/api/config/schemes", json={"name": "newScheme", "ossFloder": "new"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["id"] == "2"
|
|
|
|
|
|
def test_delete_scheme(client, tmp_config):
|
|
resp = client.delete("/api/config/schemes/1")
|
|
assert resp.status_code == 200
|
|
|
|
|
|
# ---- Branches ----
|
|
|
|
def test_get_branches(client, tmp_config):
|
|
resp = client.get("/api/config/branches")
|
|
assert resp.status_code == 200
|
|
branches = resp.json()
|
|
assert "main" in branches
|
|
assert "dev" in branches
|
|
|
|
|
|
def test_add_branch(client, tmp_config):
|
|
resp = client.post("/api/config/branches", json={"name": "release/1.0"})
|
|
assert resp.status_code == 200
|
|
branches = client.get("/api/config/branches").json()
|
|
assert "release/1.0" in branches
|
|
|
|
|
|
def test_add_duplicate_branch(client, tmp_config):
|
|
resp = client.post("/api/config/branches", json={"name": "main"})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_add_empty_branch(client, tmp_config):
|
|
resp = client.post("/api/config/branches", json={"name": ""})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_delete_branch(client, tmp_config):
|
|
resp = client.delete("/api/config/branches/dev")
|
|
assert resp.status_code == 200
|
|
branches = client.get("/api/config/branches").json()
|
|
assert "dev" not in branches
|
|
|
|
|
|
def test_delete_nonexistent_branch(client, tmp_config):
|
|
resp = client.delete("/api/config/branches/notexist")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---- Servers ----
|
|
|
|
def test_get_servers(client, tmp_config):
|
|
resp = client.get("/api/config/servers")
|
|
assert resp.status_code == 200
|
|
assert "测试环境" in resp.json()
|
|
|
|
|
|
def test_create_server(client, tmp_config):
|
|
resp = client.post("/api/config/servers", json={
|
|
"name": "新环境",
|
|
"api": "https://new.api.com",
|
|
"assDom": "applinks:new.com",
|
|
"universalLink": "https://new.com",
|
|
})
|
|
assert resp.status_code == 200
|
|
servers = client.get("/api/config/servers").json()
|
|
assert servers["新环境"]["app_id_prefix"] == 3
|
|
|
|
client.delete("/api/config/servers/新环境")
|
|
second = client.post("/api/config/servers", json={"name": "第二环境", "api": "https://second.api.com"})
|
|
assert second.status_code == 200
|
|
assert client.get("/api/config/servers").json()["第二环境"]["app_id_prefix"] == 4
|
|
|
|
|
|
def test_delete_server_in_use(client, tmp_config):
|
|
# 测试环境被 App 1 使用,应该删除失败
|
|
resp = client.delete("/api/config/servers/测试环境")
|
|
assert resp.status_code == 400
|
|
|
|
|
|
# ---- Build Settings ----
|
|
|
|
def test_get_build_settings(client, tmp_config):
|
|
resp = client.get("/api/config/build")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "max_concurrent_builds" in data
|
|
assert "build_dir_retention_hours" in data
|
|
|
|
|
|
def test_update_build_settings(client, tmp_config):
|
|
resp = client.put("/api/config/build", json={"max_concurrent_builds": 4})
|
|
assert resp.status_code == 200
|
|
assert client.get("/api/config/build").json()["max_concurrent_builds"] == 4
|
|
|
|
|
|
# ---- Versions ----
|
|
|
|
def test_update_versions_keeps_per_version_build_number(client, tmp_config):
|
|
# 首次设置某版本号,构建号从 .0 起(尚未打包)
|
|
resp = client.put("/api/config/versions", json={"app_ver": "2.196.0"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["app_ver"] == "2.196.0"
|
|
assert resp.json()["build_ver"] == "2.196.0.0"
|
|
assert client.get("/api/config/versions").json()["build_ver"] == "2.196.0.0"
|
|
|
|
|
|
def test_update_versions_does_not_reset_used_build_number(client, tmp_config):
|
|
# 模拟 2.195.0 已打到 .5、并切到 2.196.0 后,再回退到 2.195.0
|
|
import json
|
|
with open(tmp_config, "r", encoding="utf-8") as f:
|
|
cfg = json.load(f)
|
|
cfg["versions"] = {"app_ver": "2.196.0", "build_map": {"2.195.0": 5, "2.196.0": 3}}
|
|
with open(tmp_config, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f)
|
|
|
|
resp = client.put("/api/config/versions", json={"app_ver": "2.195.0"})
|
|
assert resp.status_code == 200
|
|
# 回退到旧版本号时,构建号保留其自身已用到的最大值,不被重置
|
|
assert resp.json()["build_ver"] == "2.195.0.5"
|
|
assert client.get("/api/config/versions").json()["build_ver"] == "2.195.0.5"
|
|
|
|
|
|
def test_update_versions_rejects_invalid_app_version(client, tmp_config):
|
|
resp = client.put("/api/config/versions", json={"app_ver": "2.196"})
|
|
assert resp.status_code == 400
|
|
assert "App_Ver" in resp.json()["detail"]
|
|
|
|
|
|
def test_versions_tracks_are_independent(client, tmp_config):
|
|
# 每个分支一条轨道(main/master 为 release,其余即分支名),App_Ver 各自独立
|
|
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
|
assert client.put("/api/config/versions", json={"track": "release", "app_ver": "2.196.0"}).status_code == 200
|
|
assert client.put("/api/config/versions", json={"track": "dev", "app_ver": "2.198.0"}).status_code == 200
|
|
assert client.put("/api/config/versions", json={"track": "feature/login", "app_ver": "2.100.0"}).status_code == 200
|
|
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["branch_track"] == {"main": "release", "dev": "dev", "feature/login": "feature/login"}
|
|
assert data["tracks"]["release"]["app_ver"] == "2.196.0"
|
|
assert data["tracks"]["dev"]["app_ver"] == "2.198.0"
|
|
assert data["tracks"]["feature/login"]["app_ver"] == "2.100.0"
|
|
assert data["tracks"]["feature/login"]["build_ver"] == "2.100.0.0"
|
|
assert data["tracks"]["release"]["kind"] == "release"
|
|
assert data["tracks"]["feature/login"]["kind"] == "feature"
|
|
# 顶层字段镜像主上架轨(第一个上架分支),兼容旧客户端
|
|
assert data["primary_track"] == "release"
|
|
assert data["app_ver"] == "2.196.0"
|
|
|
|
|
|
def test_update_versions_rejects_unknown_track(client, tmp_config):
|
|
resp = client.put("/api/config/versions", json={"track": "beta", "app_ver": "2.196.0"})
|
|
assert resp.status_code == 400
|
|
|
|
|
|
def test_legacy_versions_split_per_branch(client, tmp_config):
|
|
"""最旧的单轨配置:拆成每个分支一条轨,各自沿用原来的已用构建号"""
|
|
import json
|
|
with open(tmp_config, "r", encoding="utf-8") as f:
|
|
cfg = json.load(f)
|
|
cfg["versions"] = {"app_ver": "2.195.0", "build_ver": "2.195.0.7"}
|
|
with open(tmp_config, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f)
|
|
|
|
data = client.get("/api/config/versions").json()
|
|
# main 留在 release 轨,dev 拆出自己的轨,两者都带着原来的已用构建号
|
|
assert data["branch_track"] == {"main": "release", "dev": "dev"}
|
|
for track in ("release", "dev"):
|
|
assert data["tracks"][track] == {
|
|
"kind": "release",
|
|
"app_ver": "2.195.0",
|
|
"build_ver": "2.195.0.7",
|
|
"build_map": {"2.195.0": 7},
|
|
}
|
|
# 拆分后各轨独立:改 release 不影响 dev
|
|
assert client.put("/api/config/versions", json={"track": "release", "app_ver": "2.196.0"}).status_code == 200
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["tracks"]["release"]["app_ver"] == "2.196.0"
|
|
assert data["tracks"]["dev"]["app_ver"] == "2.195.0"
|
|
|
|
|
|
def test_legacy_shared_tracks_split_per_branch(client, tmp_config):
|
|
"""旧的共用轨:master 留在 release 轨,其余分支(含自测分支)各拆出自己的轨"""
|
|
import json
|
|
with open(tmp_config, "r", encoding="utf-8") as f:
|
|
cfg = json.load(f)
|
|
cfg["branches"] = ["main", "dev", "feature/login", "feature/pay"]
|
|
cfg["versions"] = {"tracks": {
|
|
"release": {"app_ver": "2.195.0", "build_map": {"2.195.0": 4}},
|
|
"feature": {"app_ver": "2.100.0", "build_map": {}},
|
|
}}
|
|
cfg["branch_track"] = {
|
|
"main": "release", "dev": "release",
|
|
"feature/login": "feature", "feature/pay": "feature",
|
|
}
|
|
with open(tmp_config, "w", encoding="utf-8") as f:
|
|
json.dump(cfg, f)
|
|
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["branch_track"] == {
|
|
"main": "release", "dev": "dev",
|
|
"feature/login": "feature/login", "feature/pay": "feature/pay",
|
|
}
|
|
for track in ("release", "dev"):
|
|
assert data["tracks"][track]["kind"] == "release"
|
|
assert data["tracks"][track]["build_ver"] == "2.195.0.4"
|
|
for track in ("feature/login", "feature/pay"):
|
|
assert data["tracks"][track]["kind"] == "feature"
|
|
assert data["tracks"][track]["app_ver"] == "2.100.0"
|
|
|
|
# 自测分支的版本号各自独立
|
|
assert client.put("/api/config/versions", json={"track": "feature/login", "app_ver": "2.101.0"}).status_code == 200
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["tracks"]["feature/login"]["app_ver"] == "2.101.0"
|
|
assert data["tracks"]["feature/pay"]["app_ver"] == "2.100.0"
|
|
|
|
|
|
def test_branch_track_defaults_and_override(client, tmp_config):
|
|
assert client.post("/api/config/branches", json={"name": "develop"}).status_code == 200
|
|
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
|
|
|
data = client.get("/api/config/versions").json()
|
|
# 每个分支一条自己的轨道,类型按分支名推断
|
|
assert data["branch_track"]["develop"] == "develop"
|
|
assert data["branch_track"]["feature/login"] == "feature/login"
|
|
assert data["tracks"]["develop"]["kind"] == "release"
|
|
assert data["tracks"]["feature/login"]["kind"] == "feature"
|
|
|
|
# 手动改类型:轨道名不变,只改构建号是否自增
|
|
resp = client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "release"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["track"] == "feature/login"
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["tracks"]["feature/login"]["kind"] == "release"
|
|
|
|
assert client.put("/api/config/branches/track", json={"branch": "feature/login", "track": "feature"}).status_code == 200
|
|
assert client.get("/api/config/versions").json()["tracks"]["feature/login"]["kind"] == "feature"
|
|
|
|
assert client.put("/api/config/branches/track", json={"branch": "nope", "track": "release"}).status_code == 404
|
|
assert client.put("/api/config/branches/track", json={"branch": "develop", "track": "beta"}).status_code == 400
|
|
|
|
|
|
def test_delete_branch_with_slash_in_name(client, tmp_config):
|
|
assert client.post("/api/config/branches", json={"name": "feature/login"}).status_code == 200
|
|
resp = client.delete("/api/config/branches/feature%2Flogin")
|
|
assert resp.status_code == 200
|
|
assert "feature/login" not in client.get("/api/config/branches").json()
|
|
|
|
|
|
def test_tags_crud_and_default_release_track(client, tmp_config):
|
|
"""Tag 与分支分开存放,默认建一条同名上架轨,可切换类型、可删除"""
|
|
assert client.get("/api/config/tags").json() == []
|
|
resp = client.post("/api/config/tags", json={"name": "v3.2.0"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["track"] == "v3.2.0"
|
|
assert client.get("/api/config/tags").json() == ["v3.2.0"]
|
|
assert client.get("/api/tags").json() == ["v3.2.0"]
|
|
assert "v3.2.0" not in client.get("/api/config/branches").json()
|
|
|
|
data = client.get("/api/config/versions").json()
|
|
assert data["branch_track"]["v3.2.0"] == "v3.2.0"
|
|
assert data["tracks"]["v3.2.0"]["kind"] == "release"
|
|
# 主上架轨仍按分支判断,Tag 不会抢占
|
|
assert data["primary_track"] == "release"
|
|
|
|
assert client.put("/api/config/branches/track", json={"branch": "v3.2.0", "track": "feature"}).status_code == 200
|
|
assert client.get("/api/config/versions").json()["tracks"]["v3.2.0"]["kind"] == "feature"
|
|
|
|
assert client.delete("/api/config/tags/v3.2.0").status_code == 200
|
|
assert client.get("/api/config/tags").json() == []
|
|
assert "v3.2.0" not in client.get("/api/config/versions").json()["branch_track"]
|
|
assert client.delete("/api/config/tags/v3.2.0").status_code == 404
|
|
|
|
|
|
def test_tag_and_branch_names_must_not_collide(client, tmp_config):
|
|
assert client.post("/api/config/tags", json={"name": ""}).status_code == 400
|
|
assert client.post("/api/config/tags", json={"name": "main"}).status_code == 400
|
|
assert client.post("/api/config/tags", json={"name": "release/v1"}).status_code == 200
|
|
assert client.post("/api/config/tags", json={"name": "release/v1"}).status_code == 400
|
|
assert client.post("/api/config/branches", json={"name": "release/v1"}).status_code == 400
|
|
assert client.delete("/api/config/tags/release%2Fv1").status_code == 200
|