Waline(安全) - item.html 的 path 从 window.location.href 改为剔除 ?t= 后的 URL。 私密题 /_/<domain>/<id>?t=<token> 的钥匙原本会整条发往 Waline 服务端 并落进其访问日志。只删 t、保留其他参数,公开题的评论线程键不变。 cwebp(可靠性) - ConvertGifToAnimatedWebP 改用 exec.CommandContext + 30s 上限。 原来裸 exec.Command 无超时,畸形/超大 GIF 会把子进程和上传 goroutine 一起挂住。 超时杀进程并记 Warn,调用方按既有逻辑回退为原 GIF,不影响上传成功。 投票选项长度(正确性) - vote_options.content 由 varchar(200) 放宽到 300,与配置默认值一致。 此前 201–300 字的选项通过校验后在写库阶段失败。 - 新增 db.MaxVoteOptionLen;voteOptionMaxLen() 取配置与存储上限的较小值, 校验与前端 maxlength 同源,避免配置调大后再次越界。 多选 checkbox 去重(正确性) - 抽出 appendUniqueValue,预置值与「其他」文本共用同一 seen 集合。 此前重复提交同名字段会让同一选项在统计里被计多次。 测试 - vote_dedupe_test.go:去重与顺序。 - vote_option_len_test.go:配置小于/等于/大于存储上限三种情形。 - TestVoteOptionMaxLengthRoundTrip:300 个多字节字符存回不截断。 - TestVoteOptionColumnWidens:把列改回 200 后 AutoMigrate 能扩回 300(老库升级路径)。 验证:WSL2 Debian + PostgreSQL 全量 go test ./... 27 个包全绿; 隔离实例渲染单题页确认新的 path 逻辑。
206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
// Copyright 2026 tamakyi. All rights reserved.
|
||
// Use of this source code is governed by a MIT-style
|
||
// license that can be found in the LICENSE file.
|
||
|
||
package db
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func TestVoteOptions(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
db, cleanup := newTestDB(t)
|
||
ctx := context.Background()
|
||
|
||
store := NewVoteOptionsStore(db)
|
||
|
||
for _, tc := range []struct {
|
||
name string
|
||
test func(t *testing.T, ctx context.Context, db *voteOptions)
|
||
}{
|
||
{"Create", testVoteOptionsCreate},
|
||
{"BulkCreate", testVoteOptionsBulkCreate},
|
||
{"ListByVoteID", testVoteOptionsListByVoteID},
|
||
{"DeleteByID", testVoteOptionsDeleteByID},
|
||
{"DeleteByVoteID", testVoteOptionsDeleteByVoteID},
|
||
{"CountByVoteID", testVoteOptionsCountByVoteID},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Cleanup(func() {
|
||
if err := cleanup("vote_options"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
tc.test(t, ctx, store.(*voteOptions))
|
||
})
|
||
}
|
||
}
|
||
|
||
func testVoteOptionsCreate(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
got, err := db.Create(ctx, CreateVoteOptionOptions{
|
||
VoteID: 1,
|
||
Content: "选项 A",
|
||
SortOrder: 0,
|
||
})
|
||
require.Nil(t, err)
|
||
require.Equal(t, uint(1), got.ID)
|
||
require.Equal(t, "选项 A", got.Content)
|
||
}
|
||
|
||
func testVoteOptionsBulkCreate(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
got, err := db.BulkCreate(ctx, 1, []string{"A", "B", "C"})
|
||
require.Nil(t, err)
|
||
require.Len(t, got, 3)
|
||
|
||
// SortOrder 应按传入顺序从 0 递增
|
||
require.Equal(t, 0, got[0].SortOrder)
|
||
require.Equal(t, 2, got[2].SortOrder)
|
||
require.Equal(t, "C", got[2].Content)
|
||
}
|
||
|
||
func testVoteOptionsListByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
_, err := db.BulkCreate(ctx, 1, []string{"B", "A", "C"})
|
||
require.Nil(t, err)
|
||
_, err = db.BulkCreate(ctx, 2, []string{"X"})
|
||
require.Nil(t, err)
|
||
|
||
got, err := db.ListByVoteID(ctx, 1)
|
||
require.Nil(t, err)
|
||
require.Len(t, got, 3)
|
||
require.Equal(t, "B", got[0].Content, "按 SortOrder 升序:B(sort 0), A(sort 1), C(sort 2)")
|
||
require.Equal(t, "C", got[2].Content)
|
||
}
|
||
|
||
func testVoteOptionsDeleteByID(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
_, err := db.Create(ctx, CreateVoteOptionOptions{VoteID: 1, Content: "A"})
|
||
require.Nil(t, err)
|
||
|
||
t.Run("normal", func(t *testing.T) {
|
||
require.Nil(t, db.DeleteByID(ctx, 1))
|
||
got, err := db.ListByVoteID(ctx, 1)
|
||
require.Nil(t, err)
|
||
require.Len(t, got, 0)
|
||
})
|
||
|
||
t.Run("not found", func(t *testing.T) {
|
||
require.Equal(t, ErrVoteOptionNotExist, db.DeleteByID(ctx, 404))
|
||
})
|
||
}
|
||
|
||
func testVoteOptionsDeleteByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
_, err := db.BulkCreate(ctx, 1, []string{"A", "B", "C"})
|
||
require.Nil(t, err)
|
||
|
||
require.Nil(t, db.DeleteByVoteID(ctx, 1))
|
||
got, err := db.ListByVoteID(ctx, 1)
|
||
require.Nil(t, err)
|
||
require.Len(t, got, 0)
|
||
}
|
||
|
||
func testVoteOptionsCountByVoteID(t *testing.T, ctx context.Context, db *voteOptions) {
|
||
_, err := db.BulkCreate(ctx, 1, []string{"A", "B"})
|
||
require.Nil(t, err)
|
||
|
||
got, err := db.CountByVoteID(ctx, 1)
|
||
require.Nil(t, err)
|
||
require.Equal(t, int64(2), got)
|
||
|
||
got, err = db.CountByVoteID(ctx, 404)
|
||
require.Nil(t, err)
|
||
require.Equal(t, int64(0), got)
|
||
}
|
||
|
||
// TestVoteOptionMaxLengthRoundTrip 保证「配置允许的长度」真的能存下。
|
||
//
|
||
// 曾经 conf 允许 300 字而列宽只有 200:201–300 字的选项通过校验后在写库时才失败
|
||
// (PostgreSQL 报 value too long,MySQL 严格模式同样报错),用户看到的是「内部错误」。
|
||
func TestVoteOptionMaxLengthRoundTrip(t *testing.T) {
|
||
conn, cleanup := newTestDB(t)
|
||
t.Cleanup(func() {
|
||
if err := cleanup("vote_options"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
|
||
ctx := context.Background()
|
||
store := NewVoteOptionsStore(conn)
|
||
|
||
// 结构体标签不能引用常量,列宽与 MaxVoteOptionLen 是两处独立字面量,
|
||
// 这里显式比一次,防止只改了一边。
|
||
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
|
||
require.Nil(t, err)
|
||
for _, ct := range columns {
|
||
if ct.Name() != "content" {
|
||
continue
|
||
}
|
||
if length, ok := ct.Length(); ok {
|
||
require.EqualValues(t, MaxVoteOptionLen, length, "vote_options.content 列宽必须与 MaxVoteOptionLen 一致")
|
||
}
|
||
}
|
||
|
||
// 用多字节字符填满上限,顺便验证按字符而非字节计数。
|
||
content := ""
|
||
for len([]rune(content)) < MaxVoteOptionLen {
|
||
content += "選"
|
||
}
|
||
|
||
option, err := store.Create(ctx, CreateVoteOptionOptions{VoteID: 1, Content: content})
|
||
require.Nil(t, err)
|
||
|
||
got, err := store.ListByVoteID(ctx, 1)
|
||
require.Nil(t, err)
|
||
require.Len(t, got, 1)
|
||
require.Equal(t, option.ID, got[0].ID)
|
||
require.Equal(t, MaxVoteOptionLen, len([]rune(got[0].Content)), "存进去的字符数应与上限一致,未被截断")
|
||
}
|
||
|
||
// TestVoteOptionColumnWidens 验证老库升级路径:已经存在 varchar(200) 的列,
|
||
// 跑一次 AutoMigrate 后必须变成 MaxVoteOptionLen。
|
||
//
|
||
// 只改 model 标签是不够的 —— 如果 AutoMigrate 不扩列,老部署上线后
|
||
// 201–300 字的选项依旧写不进去,而新库(测试库)永远发现不了这个问题。
|
||
func TestVoteOptionColumnWidens(t *testing.T) {
|
||
conn, cleanup := newTestDB(t)
|
||
t.Cleanup(func() {
|
||
if err := cleanup("vote_options"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
})
|
||
|
||
// 退回老库的列宽(两种方言语法不同)。
|
||
var alter string
|
||
switch conn.Dialector.Name() {
|
||
case "postgres":
|
||
alter = "ALTER TABLE vote_options ALTER COLUMN content TYPE varchar(200)"
|
||
case "mysql":
|
||
alter = "ALTER TABLE vote_options MODIFY COLUMN content varchar(200)"
|
||
default:
|
||
t.Skipf("未适配的方言:%s", conn.Dialector.Name())
|
||
}
|
||
require.Nil(t, conn.Exec(alter).Error)
|
||
|
||
contentLength := func() int64 {
|
||
columns, err := conn.Migrator().ColumnTypes(&VoteOption{})
|
||
require.Nil(t, err)
|
||
for _, ct := range columns {
|
||
if ct.Name() != "content" {
|
||
continue
|
||
}
|
||
length, ok := ct.Length()
|
||
require.True(t, ok, "content 列应有明确长度")
|
||
return length
|
||
}
|
||
t.Fatal("未找到 content 列")
|
||
return 0
|
||
}
|
||
require.EqualValues(t, 200, contentLength(), "前置条件:列宽应已被改回 200")
|
||
|
||
require.Nil(t, conn.AutoMigrate(&VoteOption{}))
|
||
require.EqualValues(t, MaxVoteOptionLen, contentLength(), "AutoMigrate 必须把列宽扩到 MaxVoteOptionLen")
|
||
}
|