26.9:版本号统一 + CI 精简 + 前端产物重建
CI 测试 / go vet + go test (push) Successful in 49s

- 全项目版本号统一:v3.x 迭代号(26.9/26.9/26.9/26.9 及裸 v2/v3)→ 26.9,
  覆盖 Go 注释 / 文档 / openapi.yaml / README×4 / 前端源码(80+ 处)
- v31_test.go 更名 custom_code_test.go;TestV2AccessorDefaults → TestKVAccessorDefaults
- docs/api/00-overview.md 更新日志合并为单条 26.9 条目(修复错位拼接)
- .goreleaser.yaml 头部注释与实际一致(Pro 2.18.1 / GITEA_TOKEN / semver tag 要求)
- CI:release-image.yml → ci.yml,仅保留 vet+test 门禁;
  镜像发布移交 GoReleaser Pro(原 build-push 的 tag 校验与 26.9 版本方案冲突,历史 9 次失败)
- 前端重建:server/web/dist 与 web-embed 同步(docs 文案嵌入更新)
This commit is contained in:
2026-09-08 03:16:51 +08:00
parent 27432218c4
commit 84df9996cb
122 changed files with 247 additions and 12268 deletions
+170
View File
@@ -0,0 +1,170 @@
package api
// 26.9:自定义提取码与站点域名单元测试。
import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
// postForm 以 urlencoded 表单调用 handler26.9 测试辅助)。
func postForm(d *Deps, path string, fields map[string]string) *httptest.ResponseRecorder {
form := url.Values{}
for k, v := range fields {
form.Set(k, v)
}
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
var handler gin.HandlerFunc
switch path {
case "/share/text":
handler = d.shareText
default:
handler = func(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) }
}
return invoke(handler, req)
}
func TestValidatePickupCode(t *testing.T) {
// 合法:空(用随机码)
if err := validatePickupCode(""); err != nil {
t.Fatalf("空码应合法: %v", err)
}
// 合法:5-8 位字母数字(L3:最小长度由 4 提升至 5)
for _, c := range []string{"abcde", "AB123", "12345678", "a1B2c"} {
if err := validatePickupCode(c); err != nil {
t.Fatalf("合法码 %s 不应报错: %v", c, err)
}
}
// 非法:长度(4 位及以下不再允许)
for _, c := range []string{"abcd", "a1B2", "abc", "123456789"} {
if err := validatePickupCode(c); err == nil {
t.Fatalf("非法长度 %s 应报错", c)
}
}
// 非法:字符
for _, c := range []string{"ab c1", "提码", "ab-cd", "ab.cd", "ab+cd"} {
if err := validatePickupCode(c); err == nil {
t.Fatalf("非法字符 %s 应报错", c)
}
}
}
func TestNormalizeSiteDomain(t *testing.T) {
// 空 = 当前地址
d, err := normalizeSiteDomain("")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d != "" {
t.Fatalf("want empty, got %q", d)
}
// 完整 URL
d, err = normalizeSiteDomain("https://share.example.com")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d != "https://share.example.com" {
t.Fatalf("want https://share.example.com, got %q", d)
}
// 带端口 + 去尾斜杠
d, err = normalizeSiteDomain("http://192.168.1.5:8466/")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d != "http://192.168.1.5:8466" {
t.Fatalf("want http://192.168.1.5:8466, got %q", d)
}
// 裸主机自动补 http
d, err = normalizeSiteDomain("share.example.com")
if err != nil {
t.Fatalf("unexpected err: %v", err)
}
if d != "http://share.example.com" {
t.Fatalf("want http://share.example.com, got %q", d)
}
// 非法:路径 / 协议
for _, bad := range []string{"https://a.com/path", "ftp://a.com", "javascript:alert(1)"} {
if _, err := normalizeSiteDomain(bad); err == nil {
t.Fatalf("非法域名 %s 应报错", bad)
}
}
}
// TestShareTextTextPlainCompat 复刻真实浏览器请求形态:
// 旧前端 bundle 发 text/plain Content-Type + urlencoded bodyfetch 字符串 body 默认头)。
// 修复前:该形态被静默存成空文本(bug 1)或 400「分享内容不能为空」(bug 2)。
func TestShareTextTextPlainCompat(t *testing.T) {
d := newPolicyTestDeps(t)
body := strings.NewReader("text=111&expire_value=1&expire_style=day&code=")
req := httptest.NewRequest(http.MethodPost, "/share/text", body)
req.Header.Set("Content-Type", "text/plain;charset=UTF-8")
w := invoke(d.shareText, req)
if w.Code != http.StatusOK {
t.Fatalf("text/plain+urlencoded 应 200: %d %s", w.Code, w.Body.String())
}
// JSON 体但 Content-Type 缺失/为 text/plain 也应可解析
req2 := httptest.NewRequest(http.MethodPost, "/share/text",
strings.NewReader(`{"text":"无头JSON","expire_value":1,"expire_style":"day"}`))
req2.Header.Set("Content-Type", "text/plain;charset=UTF-8")
w2 := invoke(d.shareText, req2)
if w2.Code != http.StatusOK {
t.Fatalf("text/plain+JSON体 应 200: %d %s", w2.Code, w2.Body.String())
}
// 取件确认内容真实落库
req3 := httptest.NewRequest(http.MethodPost, "/share/select",
strings.NewReader(`{"code":"`+codeOf(w)+`"}`))
req3.Header.Set("Content-Type", "application/json")
w3 := invoke(d.shareSelectPost, req3)
if !strings.Contains(w3.Body.String(), "111") {
t.Fatalf("落库内容应为 111: %s", w3.Body.String())
}
}
// codeOf 从创建响应提取取件码。
func codeOf(w *httptest.ResponseRecorder) string {
var env struct {
Data struct {
Code string `json:"code"`
} `json:"data"`
}
_ = json.Unmarshal(w.Body.Bytes(), &env)
return env.Data.Code
}
func TestShareTextCustomCode(t *testing.T) {
d := newPolicyTestDeps(t)
// 自定义码成功创建
w := postForm(d, "/share/text", map[string]string{"text": "自定义码测试", "code": "MYCODE1"})
if w.Code != http.StatusOK {
t.Fatalf("自定义码创建失败: %d %s", w.Code, w.Body.String())
}
// 重复占用 → 400
w = postForm(d, "/share/text", map[string]string{"text": "第二条", "code": "MYCODE1"})
if w.Code != http.StatusBadRequest {
t.Fatalf("占用码应 400: %d %s", w.Code, w.Body.String())
}
// 非法码 → 400
w = postForm(d, "/share/text", map[string]string{"text": "第三条", "code": "abc"})
if w.Code != http.StatusBadRequest {
t.Fatalf("过短码应 400: %d %s", w.Code, w.Body.String())
}
// 空码 → 随机码仍正常
w = postForm(d, "/share/text", map[string]string{"text": "第四条", "code": ""})
if w.Code != http.StatusOK {
t.Fatalf("空码应回退随机: %d %s", w.Code, w.Body.String())
}
}